Skip to content

附录 G · 端点矩阵与数据复用 ​

附录 F 回答了"某个端点长什么样"。这一页回答另一类问题:官服到底有几个端点、各自用什么方法、哪些数据结构被反复复用。

方法同上:登录后用 JWT 把官服自己下发的 endpoints 清单逐个打一遍(而不是猜路径),再对各结构做字段集交叉比对。

1. endpoints 是随登录态增长的 ​

同一个 GET /,带不带 JWT 拿到的端点表不一样:

无 JWT带 JWT
端点个数1522
my_player / my_items / my_draftnull填上了(/players/<id>、/items/<id>、/draft/<id>)
campaign.* 六件套不存在campaign、campaign.reset、campaign.upgrade、campaign.strategy、campaign.victory、campaign.abort_victory

两点值得注意:

  • 键名里带点号:campaign.abort_victory 这样的 key 直接出现在 JSON 里(客户端大概按前缀切分后映射到枚举)。自建服务若要复刻,照抄这种命名即可。
  • campaign 族只在登录后出现:未登录时连 key 都没有,不是值为 null。写服务端时"按登录态裁剪端点表"是官方行为,不是可选项。

2. current_user 就是 JWT 的解码结果 ​

带 JWT 时 GET / 返回的 current_user 有 15 个字段,与令牌 claim 逐一对应、完全一致:

text
client_id, exp, external_id, iat, identity_id, iss, jti,
language, payment, player_id, provider, roles, tier, user_id, user_name

这解释了两件事:为什么服务端只需要一个 token 就能渲染出用户信息;以及为什么"exp 填用户 ID"这种怪做法能过——客户端读的是解码后的字段,不是去校验它。

3. 端点矩阵(实测) ​

端点方法状态返回形态备注
/GET200{build_info, current_user, endpoints, host_info, server_time, service_name, tenant_name, tier_name}端点表随登录态变化
/sessionPOST200约 70 个扁平字段见附录 F
/configGET200{xserver_closed, xserver_closed_header, forgot_password_url}唯一配置口
/players/{id}GET405—my_player 是写接口,不能 GET
/playersGET405—集合也不可 GET(搜索应是 POST)
/players/{id}/decksGET200裸数组 [5]元素字段见第 4 节
/players/{id}/libraryGET200{cards, new_cards}条目主键 card_type
/players/{id}/packsGET200[]空集合
/players/{id}/achievementsGET200[]空集合
/players/{id}/dailymissionsGET200{missions}missions[]:canceled, completed_date, counter, create_date, details, id, mission_id, modify_date, player_id, slot
/items/{id}GET200{date, equipped_items, items}items[]:cnt, details, item_id
/draft/GET200{cost, rewards}抽卡赛元数据
/draft/{id}GET200{cards, losses, status, wins}cards[]:card_count, total_cards
/matches/v2/(未匹配)GET200null见第 6 节
/matches(未匹配)GET200null同上
/lobbyplayersGET200[]大厅玩家列表
/tourney/GET200{all_knockout_tourneys}元素:end_date, id, name, rules_json_str, start_date
/store/GET200空体(0 字节)不是 JSON,就是什么都没有
/store/v2/GET200{alwaysFeatured, currency, groups, message, status, ts}groups[]:endDate, group, groupId, hidden, offers, startDate;offers[] 25 字段(offerId, offerName, gold, diamonds, real, discount, entitlementName, slotType, slotValue, …)
/store/txnGET200{campaigns, cards, decks, diamonds, draft_admissions, equipment, gold, packs, resources}交易流水聚合
/campaign/{id}GET200[]战役进度(本账号为空)
/campaign/{id}/{reset,upgrade,strategy,victory,abort_victory}—405—全是写操作,未触发

405 比 404 有信息量

405 Method Not Allowed 说明路由存在、只是方法不对;404 说明路径根本不存在(如 /.com/config)。排障时先分清这两者,能省掉一半猜测。

4. 复用证据:字段集完全相同的结构对 ​

对每个结构的字段名集合两两求交,以下是完全一致的(即"同一批对象被换了地方再发一次"):

AB结论
/session → decks.headers[]GET /players/{id}/decks[]11 个字段逐一相同(ally_faction, card_back, create_date, deck_code, favorite, id, last_played, main_faction, modify_date, name, player_id)——会话把卡组列表内联了,端点只是同一批对象换个容器
/session → all_knockout_tourneys[]GET /tourney/ 的元素同为 {end_date, id, name, rules_json_str, start_date},会话内联了赛事
/session → new_cardslibrary.new_cards同名同用途(两处都为空,只能确认名称与位置)
view_offers.groups[]view_offers.alwaysFeatured[]同一份 payload 内复用同一个 group 结构
/session → current_userJWT claim 集15 字段完全一致(第 2 节)

实践含义:服务端不必为 /session 和各个子端点各写一套序列化——同一批 DTO 内联到哪里都一样。反过来说,只要改变其中一个结构的字段,客户端会在它出现的每个位置同时受影响。

5. 同名不同形:最容易踩的六个键 ​

用户说的"不同端点不同格式"主要体现在同一个键名在不同端点里是不同类型:

键形态 A形态 B
decks/session → 对象 { "headers": [ … ] }GET /players/{id}/decks → 裸数组 [ … ]
server_time/session → 2026.09.25-06.03.07(点分)/ → 2026-09-25T06:00:00.420343Z(ISO 6 位小数)
cardslibrary.cards = 卡牌行数组transactions.cards = 数量;my_draft.cards = {card_count, total_cards}
id卡组 id = 卡组数字 ID卡牌行里官服没有 id(只有 player_id);私服(fyserver)会多回一个数字 id——客户端两种都不依赖
datemy_items.date = 整份物品的时间戳missions[].create_date / completed_date = 单条任务时间
currency/session.currency = "USD"/store/v2/.currency = "USD"(实测两者都是字符串,未发现差异)

decks 的两种形态是最典型的坑

/session 里必须包一层 {"headers": [...]},独立端点则必须是裸数组。同一批对象、两种容器——这类差异只能靠实测发现,无法从命名推断。

6. 空响应的三种语义(实测原文) ​

"没有数据"在这个 API 里有三种写法,客户端三种都认,但含义不同:

原文出现位置含义
[]/players/{id}/packs、/achievements、/lobbyplayers、/campaign/{id}空集合
null/matches/v2/、/matchesJSON null 字面量(未匹配)
(0 字节)/store/空体,连 JSON 都不是

纠正:未匹配时官服返回的是 JSON null,不是字符串 "null"

正文第 7 章与附录 D 曾断言"必须返回字符串 null,JSON null 会让客户端卡在排队转圈"。官服实测返回的正是 JSON null(4 字节 null),而客户端在官服上工作正常——所以两种都能过。真正的禁忌只有一个:别返回 {} 或 []。附录 D 的断言已据此更正。

7. 对局内端点(真机抓包 + 官服实测) ​

第 3 节的矩阵只覆盖只读接口。对局内的端点来自两条独立证据:真机客户端的完整调用序列(附录 H)与官服实测。

端点方法状态返回形态备注
/singleplayerlobbyPOST200开局载荷人机进局(正文第 13 章)
/matches/v2/GET200开局载荷顶层只有 match_and_starting_data;私服会多一个 local_subactions
/matches/v2/{id}GET200裸文本 running / finished保活 兼"我方载入完成"信号
/matches/v2/reconnectGET200裸文本 null无可重连对局
/matches/v2/{id}/actionsPUT200{actions, match, opponent_polling}轮询;无新动作时 actions 整个省略;match 只有 3 个字段
/matches/v2/{id}/actionsPOST200OK(纯文本)提交;官服按真机格式也回 400 ACTION_ERROR(门槛未解,见第 13 章第九节)
/matches/v2/{id}/mulliganPOST200{ai_error, deck, replacement_cards}换牌;PUT 返回 405
/matches/v2/{id}/mulligan/{location}GET200换牌结果,未换牌时是空对象 {}location = left / right
/matches/v2/{id}PUT200OK会话动作({a: 包},包内 {side, action, value}):投降/结束
/matches/v2/{id}/postGET200{faction, winner}赛后面板;faction 首字母大写(来源:私服抓包)

三个"看着像 JSON 其实不是"的响应

GET /matches/v2/{id}、GET /matches/v2/reconnect 返回的是裸文本(running、null),POST .../actions 返回 OK——都不是 JSON。写客户端兼容层时别一律 json.loads。

8. 客户端会调、但私服没实现的端点(真机抓包) ​

这张表来自真机客户端打到自建私服的 404 清单——它们是"客户端期望存在"的证据,不代表官服一定有:

端点私服官服客户端反应
/players/{id}/achievements404200 [](空集合)忽略,继续
/players/{id}/dailymissions404200 {missions}忽略,继续
/players/{id}/packs404200 []忽略,继续
/store/txn/dlc404未实测忽略,继续
/players/{codec 包}404未实测忽略,继续(PUT,accept-eula 等玩家事件;玩家标识编码在 URL 路径里)

404 不致命,但会浪费时间

上表这些 404 不会让客户端崩——它会继续往下走。所以排查"进不去游戏"时别盯着 404 看,真正卡住流程的是返回值形态不对的端点(比如 /matches/v2/ 该给 null 却给了 [])。

9. 本页未覆盖 ​

保持边界清楚——以下都没有实测,因为它们会改动账号数据、需要更长对局,或仍无解:

项原因
campaign.* 的五个写操作会推进/重置战役进度
/store/v2/txn(购买)与 /email/set会产生真实交易
官服动作提交的格式门槛按真机格式提交仍被 400 拒,见第 13 章第九节
出牌/攻击类动作的 action_data 字段现有真机样本只有 4 种动作,需要一局更长的对局
heartbeat 的实际副作用会置在线状态,为保持只读而跳过
/players 的 POST(搜索)未验证参数形态