Skip to content

Latest commit

 

History

History
296 lines (254 loc) · 41.9 KB

services_account.md

File metadata and controls

296 lines (254 loc) · 41.9 KB

AccountService(账户与状态中枢)— 功能参考

逐行精读源码生成。行号基于当前工作区版本,仅用于快速定位。

文件职责:src-tauri/src/services/account_service.rs(约 3320 行)是账户与状态中枢,集中管理「Hypergryph 登录(密码/验证码/扫码)→ 森空岛 cred/token → 角色列表与角色详情」的完整链路,并承担凭据持久化、双版本账户缓存(完整版/精简版)、懒加载开关、按角色授权 u8token、自动签到定时任务与前端事件通知。

u8token 粒度与来源(2026-11 二次改版):u8token(抽卡令牌)完全不持久化,每次同步现换。授权只是 roles[].u8Authorized 一个布尔开关,不含任何凭据。

换取走鹰角通行证链路:hytoken →(grant type:1, appCode be36d44aa36bfb5b)→ L2 →(u8_token_by_uid {uid})→ u8token。官服与 B 服同一条链、同一个 hytoken,差异只在渠道绑定 uid,换出的两把 token 互不通用 —— 因此 roles[] 必须保存 bindingUid,否则无法为角色换令牌。

旧的两条路线都已删除:① 账号级 account_token_*.u8token(渠道写死官服);② role_u8token_{roleId}(按角色持久化令牌,但仍走 channelMasterId=1 的旧链)。启动时 migrate_legacy_u8token 会把它们的遗留字段与凭据库条目一并清掉。

状态注册方式:lib.rs:199 调用 AccountService::new(config_service, skland_service, avatar_cache_service, network_service, app.handle().clone());lib.rs:206 包装为 Arc::new(Mutex::new(account_service))(其中 Mutex 为 tokio::sync::Mutex,见 lib.rs:3),lib.rs:207 执行 app.manage(managed_service)。因此所有 command 以 State<'_, Arc<Mutex<AccountService>>> 注入并在 .lock().await 后调用;lib.rs:215 另把克隆传给 AccountService::start_auto_refresh 启动后台定时器。注意:服务内部持有的 config_service 是 std::sync::Mutex<ConfigService>(lib.rs:151),与外层 tokio Mutex 不同。

主要依赖:

  • services:ConfigService(配置读写,account_token_* 等键)、SklandService(check_cred / refresh_cred_by_hytoken / get_u8_token_by_uid / get_role_detail / get_player_binding / extract_endfield_roles / get_user_info / get_game_list / call_skland_api)、AvatarCacheService(get_or_download_avatar 下载头像转 base64)、NetworkService(char_detail_service().get_with_cache、set_current_role_id、retain_only_char_detail、preload_all_char_details、query_role_data、get_current_role_id)。
  • models:account::{AccountInfo, AccountLoginResult, AccountRefreshResult, AccountSummary, RoleU8AuthResult, SklandAccountInfo, SklandGameInfo, SklandUserInfo}、login::{CodeLoginRequest, LoginRequest, ScanLoginInfo, ScanStatus, SendCodeRequest}、role::RoleDisplayInfo。
  • utils:http_client::create_client、capture::send(HTTP 请求 + 抓包日志)、AppError;日志宏 log_debug! / log_info! / log_warn! / log_error! 与 tracing。
  • 外部 crate:serde_json、chrono(刷新时间戳)、tauri::{AppHandle, Emitter, async_runtime}、tokio。

内部结构总览

区间 内容
1-21 行 use 导入(models / services / utils / 日志宏 / tauri)
24-26 行 常量 SKLAND_GAMES_CACHE_KEY = "skland_games_cache"、SKLAND_GAMES_CACHE_TTL_SECS = 12*60*60
24-46 行 常量与 RoleOwner 结构体(user_id / binding_uid / channel_master_id / server_id)
43-48 行 自由函数 now_unix_secs()(当前 Unix 秒)
50-55 行 struct AsyncAccountService:供 async spawn 使用的精简克隆(仅 2 个服务引用)
57-297 行 AsyncAccountService::check_and_refresh_user_cred:cred 校验、过期刷新(不再顺带取 u8token)
310-323 行 pub struct AccountService 结构体与 8 个字段
325-360 行 new 构造函数(读取 lazy_load_enabled,默认 true;调用 migrate_legacy_u8token)
338-351 行 3 个服务访问器:get_config_service / skland_service / avatar_cache_service
353-481 行 set_lazy_load_enabled:懒加载开关切换 + 双缓存互转 + 角色详情预载/裁剪
483-494 行 is_lazy_load_enabled、set_current_role_id
496-557 行 账户缓存读写:cache_account_list / cache_account_summary / get_cached_accounts / clear_account_cache
559-600 行 gather_preload_role_infos:遍历 account_token_* 收集预加载所需角色+凭据
602-617 行 clone_for_async 与对外的 check_and_refresh_user_cred(转发)
32-341 行 各服务访问器、懒加载开关与双缓存、gather_preload_role_infos、clone_for_async、check_and_refresh_user_cred
342-630 行 凭据字段写入器:set_hytoken_for_user、set_phone_for_user、set_device_token_for_user、get_local_device_token、notify_cache_refreshed、cache_accounts_by_user
632-940 行 抽卡授权:authorize_role_u8token、revoke_role_u8token、is_role_u8_authorized、fetch_role_u8token、set_role_u8_authorized、find_role_owner、migrate_legacy_u8token、backfill_binding_uid
约 1663-1810 行 账号移除:logout_account(解绑单角色)、delete_skland_account(删整账号)、batch_logout
747-781 行 get_local_device_token(私有)、set_device_token_for_user
783-817 行 notify_cache_refreshed(emit accounts-refreshed)、cache_accounts_by_user
819-973 行 自动签到三件套:get_auto_sign_roles / check_attendance_today / do_attendance_for_role
975-1016 行 start_auto_refresh:300 秒定时器(刷新账户 + 自动签到)
1018-1399 行 get_accounts:全量同步(约 380 行,含 cred 刷新、失败状态、自动刷新重试)
1401-1455 行 add_account:密码登录入口
1457-1538 行 logout_account(按 roleId 解绑角色)、batch_logout
1540-1840 行 refresh_accounts:手动/定时刷新(走 get_role_detail,完成后 emit)
1842-1890 行 send_verification_code:发送短信验证码
1892-1994 行 get_hypergryph_token_by_code(私有)、add_account_by_code:验证码登录
1996-2118 行 gen_scan_login、scan_status:扫码二维码生成与轮询
2120-2247 行 token_by_scan_code(私有)、add_account_by_scan:扫码登录
2249-2390 行 complete_login_from_token:三种登录方式的公共后续流程
2392-2477 行 fetch_endfield_roles、read_skland_credentials、cache_skland_user_info
2479-2573 行 get_skland_user_info、get_skland_games(+ 缓存读写)、get_skland_account_roles
2575-2652 行 get_skland_accounts、save_skland_account
2654-2810 行 save_selected_roles:保存勾选角色、迁移/保留凭据、构建双缓存
2812-2847 行 migrate_roles_app_field:为旧 roles 补 app: "endfield"
2849-2920 行 get_hypergryph_token(密码换取 hytoken)、is_new_device_verification_error
2922-3062 行 get_skland_code(OAuth grant)、get_skland_cred(code 换 cred/token/userId)
3064-3096 行 query_role_data(统一数据查询入口)

结构体与字段

AccountService(295-309 行)

字段 类型 含义
config_service Arc<Mutex<ConfigService>>(std Mutex) 全局配置服务,承载 account_token_*、account_list 等所有持久化键
skland_service Arc<SklandService> 森空岛 API 封装(cred 校验/刷新、角色详情、用户资料、游戏列表、attendance 等)
avatar_cache_service Arc<AvatarCacheService> 头像下载与本地缓存,返回 base64
network_service Arc<NetworkService> 角色详情缓存与统一数据查询(char_detail_service 等)
account_list_cache Arc<Mutex<HashMap<String, Vec<AccountInfo>>>> 完整版账户缓存,key 为 user_id;仅非懒加载时写入/读取
account_summary_cache Arc<Mutex<HashMap<String, Vec<AccountSummary>>>> 精简版账户缓存(无 cred/token),仅懒加载时使用
lazy_load_enabled Arc<Mutex<bool>> 懒加载开关(内存态,初始值来自配置 lazy_load_enabled)
app_handle Option<AppHandle> Tauri AppHandle,用于 emit("accounts-refreshed", …)

AsyncAccountService(37-41 行,私有)

字段 类型 含义
config_service Arc<Mutex<ConfigService>> 与主服务共享的配置服务
skland_service Arc<SklandService> cred 校验/刷新所需

MissingDeviceTokenPolicy(286-293 行,私有枚举):已删除。绑定流程不再因为缺 device token 中断,只有「密码接口自身拒绝新设备」会返回 NEW_DEVICE_VERIFICATION_REQUIRED(此时还没拿到 hytoken,无法继续)。

方法清单(必须完整)

共 57 个函数:37 个 pub、20 个私有(含 1 个自由函数、1 个静态关联函数、1 个 AsyncAccountService 方法)。私有项以「(私有)」标注。

方法 位置 说明(输入 → 做什么 → 输出;调用的外部 API/命令)
now_unix_secs()(私有自由函数) 29 无输入 → 取当前 Unix 秒(失败返回 0)→ i64
AsyncAccountService::check_and_refresh_user_cred(私有) 45 user_id → 读 account_token_{user_id} 取 cred/token/hytoken/device_token;SklandService::check_cred 有效 → Ok(None)(不再回填 u8token);失效或 check_cred 调用失败则调 refresh_cred_by_hytoken(返回三元组)换新 cred/token 写回配置 → Ok(Some((cred, token)));缺 hytoken → AppError::AuthError
new 312 4 个服务 + AppHandle → 读 lazy_load_enabled(默认 true)、初始化空缓存与开关 → AccountService
get_config_service 338 无 → 返回 &Arc<Mutex<ConfigService>>(供 command 直接读写配置,如 selected_account_id)
skland_service 343 无 → 返回 &Arc<SklandService>(commands/attendance.rs 借此调 call_skland_api)
avatar_cache_service 348 无 → 返回 &Arc<AvatarCacheService>(commands/attendance.rs 用)
set_lazy_load_enabled 353 enabled: bool → 写配置 lazy_load_enabled;开启:完整缓存转精简后清空、network_service.retain_only_char_detail(当前角色);关闭:用 account_token_* 的 cred/token 把精简合成完整、清空精简缓存、gather_preload_role_infos() + network_service.preload_all_char_details() 预载全部角色详情 → Ok(())
is_lazy_load_enabled 483 无 → 读内存开关 → bool
set_current_role_id 488 role_id: Option<String> → 取当前懒加载状态并转发 network_service.set_current_role_id(role_id, lazy) → (async,无返回值)
cache_account_list 496 user_id, Vec<AccountInfo> → 写入完整版缓存 → 无
cache_account_summary 503 user_id, Vec<AccountSummary> → 写入精简版缓存 → 无
get_cached_accounts 510 user_id → 懒加载时读精简缓存并转成 cred/token = None 的完整结构;否则读完整缓存 → Option<Vec<AccountInfo>>(commands/account.rs:40 命中则免 API)
clear_account_cache 551 user_id → 同时移除完整版与精简版缓存 → 无
gather_preload_role_infos(私有) 560 无 → 遍历所有 account_token_* 的 roles[],配对 cred/token → Vec<PreloadRoleInfo>(network_service 用)
clone_for_async(私有) 602 无 → 构造 AsyncAccountService(克隆 2 个 Arc)→ 供 spawn 使用
check_and_refresh_user_cred 610 user_id → clone_for_async() 后转发同名私有方法 → Result<Option<(String, String)>, AppError>;被 command check_and_refresh_cred、attendance/gacha command 调用
set_hytoken_for_user 619 user_id, hytoken → 在 account_token_{user_id} 对象写入 hytoken 字段(非对象则 ConfigError)→ Ok(())
set_u8token_for_user — 已删除。u8token 不再持久化
refresh_u8token_for_user — 已删除。改为抽卡同步前现换:fetch_role_u8token
is_role_u8_authorized 700 附近 role_id → 遍历 account_token_* 的 roles[],看该角色的 u8Authorized 是否为 true → bool
authorize_role_u8token 640 role_id → find_role_owner 取 user_id + binding_uid → fetch_role_u8token 现换一次令牌做校验(不保存)→ 成功则 set_role_u8_authorized(true) 返回 authorized,失败返回 failed + 原因 → RoleU8AuthResult;command authorize_role_u8token
revoke_role_u8token 672 role_id → 找到所属账号,把该角色 roles[].u8Authorized 移除并落盘 → bool;command revoke_role_u8token
fetch_role_u8token(pub) 736 user_id, binding_uid → 读 account_token_{user_id}.hytoken(缺失报 AuthError)→ SklandService::get_u8_token_by_uid → String(调用方不得持久化);抽卡同步前调用
set_role_u8_authorized(私有) 770 user_id, role_id, authorized → 改 roles[].u8Authorized 并落盘
find_role_owner(私有) 800 遍历 account_token_* 的 roles[] 找 roleId → RoleOwner { user_id, binding_uid, channel_master_id, server_id };找不到 AuthError
resolve_arknights_target(私有) 931 role_id → 遍历 account_token_*.roles[],校验该角色 app == "arknights"(否则 ConfigError 提示数据源只服务明日方舟角色)→ (user_id, bindingUid);bindingUid 为空时报「请重新绑定」
arknights_credentials(私有) 979 user_id → read_skland_credentials 取 cred/token,再尝试 check_and_refresh_user_cred(成功用新值;失败回退旧值只记 warn,不阻断数据查询)→ (cred, token)
get_arknights_player_info 998 role_id, force → resolve_arknights_target + arknights_credentials → ArknightsService::refresh_player_info(force=false 时先读 arknights_service.get_cached_player_info)→ serde_json::Value;command get_arknights_player_info
get_arknights_cultivate 1035 role_id → 同上解析凭据 → ArknightsService::fetch_cultivate → Value;command get_arknights_cultivate
get_arknights_attendance 1053 role_id → ArknightsService::attendance_status → 包成 {code, message, data, timestamp} 外壳(arknights_attendance_envelope,与终末地响应同形,前端解析器依赖 json.data)→ Value;data 内含后端算好的 hasToday / todayIndex / tomorrowIndex;command get_arknights_attendance
do_arknights_attendance 1071 role_id → ArknightsService::do_attendance → 同样包成 {code, message, data} 外壳(code 透传真实业务码 0 / 10001);data 为 ArknightsAttendanceResult(awards[] + tomorrowAwards[],已签到时 alreadySigned = true);command do_arknights_attendance
migrate_legacy_u8token(私有静态) 836 启动时清理遗留:删除所有 role_u8token_* 键(含凭据库条目)+ 删除 account_token_*.u8token 字段与条目
backfill_binding_uid(私有) 890 拿到 binding 列表时,为老数据里缺 bindingUid 的角色按 roleId 回填 uid / channelMasterId / channelName
get_local_device_token(私有) 747 user_id → 读 account_token_{user_id}.device_token(空串视为无)→ Option<String>
set_device_token_for_user 759 user_id, device_token → 写入 device_token 字段 → Ok(())
set_phone_for_user 652 user_id, phone → 写入 account_token_{user_id}.phone(仅用于重新登录时短信/密码方式自动填充,不参与鉴权)→ Ok(())
notify_cache_refreshed(私有) 789 &AccountRefreshResult → 有 app_handle 时 emit("accounts-refreshed", result) → 无
cache_accounts_by_user(私有) 796 &[AccountInfo] → 按 user_id 分组,按懒加载状态分别写精简/完整缓存 → 无
get_auto_sign_roles 819 无 → 读配置 card_auto_sign_users → Vec<String>(role_id 列表)
check_attendance_today 1291 role_id → 先试 resolve_arknights_target:命中则走 ArknightsService::attendance_status(GET /api/v1/game/attendance?gameId=1&uid=)并取其 hasToday;否则遍历 account_token_* 找该角色的 serverId/cred/token → SklandService::call_skland_api("GET", "/web/v1/game/endfield/attendance")(头 sk-game-role: 3_{role}_{server}、token)→ 解析 data.hasToday → bool
do_attendance_for_role 1379 role_id → 先试 resolve_arknights_target:命中则走 ArknightsService::do_attendance(POST /api/v1/game/attendance);否则同上定位凭据(额外取 user_id)→ check_and_refresh_user_cred 刷新 → call_skland_api("POST", "/web/v1/game/endfield/attendance") → Ok(());code!=0 但属于 is_benign_business_error(已签到)时同样视为成功
start_auto_refresh(静态) 975 Arc<tokio::sync::Mutex<AccountService>> → async_runtime::spawn 每 300 秒:refresh_accounts() → 对 card_auto_sign_users 中未签到角色执行 do_attendance_for_role → 无;由 lib.rs:215 启动
get_accounts 1018 无 → 遍历 account_token_* 收集 roles → 逐角色 check_and_refresh_user_cred → network_service.char_detail_service().get_with_cache(...) → 成功取昵称/等级并 avatar_cache_service.get_or_download_avatar → 失败按错误串判定 HYTOKEN_EXPIRED/FAILED,并可用 hytoken refresh_cred_by_hytoken 后重试一次 → 按懒加载写缓存 → Vec<AccountInfo>
add_account 1401 LoginRequest(含可选 expectedUserId)→ get_hypergryph_token(密码) →(新设备错误经 is_new_device_verification_error 判定)返回 error_message = "NEW_DEVICE_VERIFICATION_REQUIRED" → 否则 complete_login_from_token(RequireUserVerification, expected_user_id, Some(phone)) → AccountLoginResult;command add_account
logout_account 约 1663 account_id(实为 roleId)+ keep_device_token → 从各 account_token_* 的 roles[] 移除该角色;roles 清空且 keep_device_token=false 时删除 device_token;清对应用户缓存;从 account_list 移除 → 恒 true;command logout_account。只解绑角色,保留父账户
delete_skland_account 约 1737 user_id → config.remove("account_token_{user_id}")(连带清凭据库)→ 从 account_list 移除该账号全部 roleId → 清缓存 → selected_account_id 指向被删账号时改选剩余第一个(无则置空)→ Result<bool>;command delete_skland_account
batch_logout 1523 account_ids → 从 account_list 移除,并 config.remove("account_token_{id}") → 恒 true;command batch_logout
refresh_accounts 1540 无 → 同 get_accounts 的收集/刷新逻辑,但角色详情走 skland_service.get_role_detail(绕过 char_detail 缓存),含 hytoken 自动刷新重试;完成后 cache_accounts_by_user + notify_cache_refreshed(emit)→ AccountRefreshResult;command refresh_accounts 与定时器
send_verification_code 1842 SendCodeRequest → POST https://as.hypergryph.com/general/v1/send_phone_code(body: phone/type)→ status==0 → Ok(true);command send_verification_code
get_hypergryph_token_by_code(私有) 1892 phone, code → POST https://as.hypergryph.com/user/auth/v2/token_by_phone_code → (token, deviceToken)
add_account_by_code 1961 CodeLoginRequest(含可选 expectedUserId)→ 上一步换 hytoken(deviceToken 空则置 None)→ complete_login_from_token(ContinueWithoutDeviceToken, expected_user_id, Some(phone)) → AccountLoginResult;command add_account_by_code
gen_scan_login 1996 无 → POST https://as.hypergryph.com/general/v1/gen_scan/login(appCode dd7b852d5f1dd9da)→ ScanLoginInfo { scan_id, scan_url };command gen_scan_login
scan_status 2063 scan_id → GET https://as.hypergryph.com/general/v1/scan_status?scanId=... → ScanStatus { status, scan_code, msg };command scan_status
token_by_scan_code(私有) 2120 scan_code → POST https://as.hypergryph.com/user/auth/v1/token_by_scan_code(带 X-AppCode/X-DeviceId/X-DeviceId2/X-DeviceModel/X-DeviceType/X-OSVer 头)→ (token, deviceToken, Option<hgld>)
add_account_by_scan 2211 scan_code, expected_user_id → 上一步换 hytoken(deviceToken 空转 None)→ complete_login_from_token(ContinueWithoutDeviceToken, expected_user_id, None) → AccountLoginResult;command add_account_by_scan(参数 scanCode + 可选 expectedUserId)
complete_login_from_token(私有) 2432 hy_token, device_token, expected_user_id, phone → Step2 get_skland_code、Step3 get_skland_cred → Step3.1 若 expected_user_id 非空且与登录得到的 user_id 不同,返回 error_message = "ACCOUNT_MISMATCH:<实际userId>" 且不落盘任何凭证 → 无 deviceToken 时用 userId 走 get_local_device_token 本地匹配;仍无也不中断(只记 warn,u8token 留待用户按角色授权)→ set_hytoken_for_user / set_phone_for_user(有手机号时)/ set_device_token_for_user(有时)→ AccountLoginResult { success: true, cred, token, user_id }(不返回角色,也不取 u8token)
fetch_game_roles(私有) 2762 cred, token, user_id → get_player_binding + SklandService::extract_game_roles → 逐角色组装 Vec<RoleDisplayInfo>(单角色失败仅记日志跳过);每个角色的 u8_authorized 由 is_role_u8_authorized 回填。终末地角色再调 get_role_detail 补头像与精确等级;明日方舟角色只用绑定接口给的昵称/等级/服务器(avatar_url 为空串)
read_skland_credentials(私有) 2434 user_id → 读 account_token_{user_id} 的 cred/token → (String, String);缺失抛 AuthError("Skland account/cred/token was not found")
cache_skland_user_info(私有) 2459 user_id, &SklandUserInfo → 序列化写入 account_token_{user_id}.user_info → Ok(())
get_skland_user_info 2485 user_id → check_and_refresh_user_cred → read_skland_credentials → SklandService::get_user_info → 缓存 user_info(失败仅告警)→ SklandUserInfo;command get_skland_user_info
get_skland_user_info_by_cred 2504 cred, token → SklandService::get_user_info(不读本地配置,用于「确认绑定森空岛账户」页展示头像/昵称,此时账户尚未保存)→ SklandUserInfo;command get_skland_user_info_by_cred
get_skland_games 2508 user_id, force → 先读 skland_games_cache(12 小时 TTL,非 force 且未过期直接返回)→ 否则刷新 cred + SklandService::get_game_list 写缓存;失败回退旧缓存 → Vec<SklandGameInfo>;command get_skland_games
read_skland_games_cache(私有) 2550 无 → 读 skland_games_cache 的 fetched_at/games → Option<(i64, Vec<SklandGameInfo>)>(空列表视为无缓存)
cache_skland_games(私有) 2563 &[SklandGameInfo] → 写 skland_games_cache = { fetched_at, games } → Ok(())
get_skland_account_roles 2575 user_id → 刷新 cred → read_skland_credentials → fetch_endfield_roles → AccountLoginResult { available_roles: Some(..) };command get_skland_account_roles
get_skland_accounts 2601 无 → 扫描 account_token_*,skland_account_bound(缺省时以 roles 非空判定)为真才收录,附带 user_info 的昵称/头像与首次登录记录的 phone → Vec<SklandAccountInfo>;command get_skland_accounts
save_skland_account 2628 cred, token, user_id → 写 cred/token/skland_account_bound=true,并保证 roles 存在(默认 [])→ Ok(());command save_skland_account
save_selected_roles 3006 cred, token, user_id, selected_roles → 先 migrate_roles_app_field;读旧数据保留 hytoken/device_token/phone/user_info;写入 roles[{userId,serverId,roleId,app,nickname,level,bindingUid,channelMasterId,channelName}](app 取 RoleDisplayInfo.game,空则 endfield)、skland_account_bound=true;更新 account_list(移除旧 roleId、追加新 roleId);锁释放后逐角色下载头像 → 写完整+精简双缓存 → Vec<AccountInfo>;command save_selected_roles
migrate_roles_app_field(私有静态) 3248 &mut ConfigService → 遍历 account_token_*,为缺少 app 的角色补 "endfield" 并写回(该字段在支持明日方舟之前不存在,那时的角色只可能来自终末地)→ Ok(())
get_hypergryph_token(私有) 2849 phone, password → POST https://as.hypergryph.com/user/auth/v1/token_by_phone_password → (token, Option<deviceToken>, Option<hgld>)
is_new_device_verification_error(私有静态) 2922 &AppError → 关键词匹配("新设备"/"需要验证"/"设备验证"/"verify"/"verification")→ bool
get_skland_code(私有) 2932 hy_token → POST https://as.hypergryph.com/user/oauth2/v2/grant(appCode 4ca99fa6b56cc2ba, type 0)→ data.code → String
get_skland_cred(私有) 2989 sk_code → POST https://zonai.skland.com/api/v1/user/auth/generate_cred_by_code(kind 1)→ (cred, token, user_id)
query_role_data 3064 role_id, api_name, paths → get_accounts() 找到该角色(需 cred/token/user_id/server_id 齐全)→ network_service.query_role_data(...) → HashMap<String, Value>(key 为请求路径);command query_role_data,另被 commands/gacha.rs:479 以 "wiki_catalog" 调用

关键业务流程

登录:密码(1401-1455、2849-2920 行)

add_account(LoginRequest) → get_hypergryph_token(POST /user/auth/v1/token_by_phone_password)得 (hytoken, deviceToken?, hgld?);若 API 报错且 is_new_device_verification_error 命中,直接返回 NEW_DEVICE_VERIFICATION_REQUIRED。随后进入 complete_login_from_token(RequireUserVerification)。注意:hgld 目前仅在日志中使用,本地 device token 匹配实际以 Step 3 返回的 user_id 为键。

登录:验证码(1842-1994 行)

send_verification_code(POST /general/v1/send_phone_code)→ add_account_by_code(CodeLoginRequest) → get_hypergryph_token_by_code(POST /user/auth/v2/token_by_phone_code)→ complete_login_from_token(ContinueWithoutDeviceToken)。

登录:扫码(1996-2247 行)

gen_scan_login(POST /general/v1/gen_scan/login)产出二维码 scanUrl → 前端轮询 scan_status(GET /general/v1/scan_status)取 scanCode → add_account_by_scan(scan_code) → token_by_scan_code(POST /user/auth/v1/token_by_scan_code,附固定 X-Device* 头)→ complete_login_from_token(ContinueWithoutDeviceToken)。

登录公共流程与设备令牌(2432-2620 行)

Step 2 get_skland_code(POST /user/oauth2/v2/grant)→ Step 3 get_skland_cred(POST zonai.skland.com/.../generate_cred_by_code)得 (cred, token, user_id) → Step 3.1 重新登录校验(expected_user_id 非空且与实际 user_id 不同 → 返回 ACCOUNT_MISMATCH:<实际userId>,不保存任何凭证,用于「扫码/短信授权到了别的账号」的拦截;前端据此清空本次登录结果并提示)→ Step 3.5 服务器未下发 deviceToken 时用 get_local_device_token(user_id) 本地匹配 → 仍缺失也继续绑定(只记 warn;该账号暂时无法获取 u8token)→ 依次落盘 hytoken、phone(本次登录带手机号时)、device_token(有时)→ 返回父级凭据,角色绑定推迟到 save_selected_roles。

改版说明:device_token 与抽卡令牌无关了(新链路不需要它)。缺它不再阻断登录,也不再触发强制新设备验证。

抽卡令牌授权与换取(640-940 行)

  • authorize_role_u8token(role_id):find_role_owner 拿到 binding_uid → 现换一次令牌做校验(令牌不保存)→ 成功才把 roles[].u8Authorized = true 落盘。所以「授权」= 验证一次 + 记一个开关。
  • revoke_role_u8token(role_id):只移除 u8Authorized 开关,没有凭据需要删除。
  • 抽卡同步前由 fetch_role_u8token(user_id, binding_uid) 现换令牌;同步命令还会先检查 u8Authorized,未授权的角色直接报错提示去账号页授权。
  • 官服与渠道服没有分支:同一条 u8_token_by_uid 链路,差异只在 binding_uid。
  • 解绑/登出:授权开关随 roles[] 一起消失(令牌本就不落盘)。
  • backfill_binding_uid 负责把老数据的渠道 uid 补上(否则旧账号无法换令牌)。

账户不可用与重新登录(前端配合)

账户数据获取频繁失败时应用照常进入(api_health 的 app:api-health 只收起 Splash,非致命),账户页与侧边栏展示「账号不可用」并提供「重新登录」入口:

  • 登录方式选择页不要求输入手机号,只需选择方式;选短信/密码后才使用/输入手机号(首次登录成功即把手机号写入 account_token_{user_id}.phone,get_skland_accounts 一并返回,供自动填充)。
  • 扫码方式在授权成功后用 expected_user_id 校验是否为同一森空岛账户,不一致即拦截(上方 Step 3.1),凭证不会落盘。
  • 数美设备指纹接口连续失败(ApiFailureKind::DeviceFingerprint)时前端提示「重启电脑并同步时区」;修复后点击重试会调用 reset_api_health 解除熔断。

token 与 cred 刷新(57-297、610-781 行)

  • 主入口 check_and_refresh_user_cred(610)转发到 AsyncAccountService(57):check_cred 有效 → 直接返回 Ok(None)(不再回填 u8token);失效或 check_cred 报错 → refresh_cred_by_hytoken(hyt token, device_token) 换新 cred/token 并写回 account_token_{user_id};无 hytoken → AuthError(提示 re-login)。
  • refresh_cred_by_hytoken 已删除 u8 分支(原 Step 1.5),返回 (cred, token, user_id) 三元组 —— 换 cred 不再有任何「顺手取令牌」的副作用。
  • get_accounts / refresh_accounts 内部:每次取角色详情前刷新;错误串含 hytoken/OAuth/grant failed → 标记 HYTOKEN_EXPIRED;错误串含 API error/Failed to parse JSON/HTTP request failed → 视为同步失败,再用 hytoken 自动刷新并重试一次详情接口。
  • u8token 不再「续期」,而是每次同步前用 fetch_role_u8token 现换(不缓存、不落盘)。

角色数据查询与缓存(496-600、1018-1399、1540-1840、3064-3096 行)

  • get_accounts(1018):走 network_service.char_detail_service().get_with_cache(带角色详情缓存),结果按 user_id 分组后按懒加载状态写入完整/精简缓存。
  • refresh_accounts(1540):走 skland_service.get_role_detail(实时接口,不经 char_detail 缓存),完成后 cache_accounts_by_user + emit。
  • query_role_data(3064):统一数据查询入口,依赖 get_accounts() 提供凭据,再委托 NetworkService::query_role_data 按 paths 取叶节点。
  • 缓存四件套(496/503/510/551)+ gather_preload_role_infos(560)支撑懒加载与预载。

森空岛用户资料与游戏列表(2434-2573 行)

get_skland_user_info → 刷新 cred → read_skland_credentials → SklandService::get_user_info → 写 account_token_{user_id}.user_info(供 get_skland_accounts 列表展示昵称/头像)。get_skland_games → skland_games_cache(12 小时 TTL,force 可绕过;刷新失败回退旧缓存)。get_skland_account_roles → fetch_endfield_roles(binding → 逐角色 detail → 头像)。

游戏分组:终末地 / 明日方舟

一个森空岛账户可以同时绑定《明日方舟:终末地》和《明日方舟》,两者的角色都进同一个 roles[],用 app 字段区分(endfield / arknights)。差异只在与「怎么拿角色展示信息」:

终末地 明日方舟
展示数据来源 GET /api/v1/game/endfield/card/detail GET /api/v1/game/player/info?uid=(由 ArknightsService 拉取)
头像 详情接口的 base.avatarUrl,下载转 base64 player/info 的 status.avatar.url,同样转 base64(这是明日方舟唯一的头像来源)
账户列表(get_accounts) 走 char_detail_service().get_with_cache 读 ArknightsService 缓存(内存 → <app_data>/arknights/player_info_{uid}.json),缓存缺失才回退 roles[] 里的绑定快照(AccountService::arknights_account)
手动刷新(refresh_accounts) 走 skland_service.get_role_detail 调 ArknightsService::refresh_player_info 重拉;失败不标 FAILED(还有缓存可用,标了会让账户页误报「账号不可用」)
懒加载预载 参与 跳过(gather_preload_role_infos 过滤掉)
query_role_data 支持 直接报错(该数据源只服务终末地)
公开方法 query_role_data get_arknights_player_info / get_arknights_cultivate / get_arknights_attendance / do_arknights_attendance

关键实现:

  • SklandService::extract_game_roles 是唯一入口,按 app_code 识别游戏并跳过 is_delete 的渠道绑定;未知 app_code 记 debug 日志后忽略。
  • AccountService::fetch_game_roles 按 game 分支:终末地角色调 get_role_detail 补头像与精确等级;明日方舟角色调 ArknightsService::refresh_player_info(绑定时就拉一次,昵称/等级/头像一并确定并落盘缓存)。拉取失败不阻断绑定,退回绑定接口给的昵称/等级、头像留空。
  • save_selected_roles 把 app / nickname / level / bindingUid 一并落盘 —— bindingUid 既是换抽卡令牌的键,也是明日方舟所有数据接口的寻址键(?uid=)。
  • backfill_binding_uid 除了补 bindingUid,还会给缺 app 的老角色按绑定列表补上正确的游戏(而不只是 endfield)。
  • RoleDisplayInfo.game / AccountInfo.game / AccountSummary.game 三处都带 #[serde(default = "default_game")],老缓存/老配置缺字段时按终末地处理。
  • 解绑/删号会连带清明日方舟缓存:logout_account 与 delete_skland_account 收集该账号下 app == "arknights" 角色的 bindingUid,调 ArknightsService::clear_cache(uid) 删掉 player_info_{uid}.json 与原始留档 —— 否则换绑别的明日方舟账号会读到上一个角色的数据。注意两处都必须先 drop(config) 再清缓存(std::sync::Mutex 不可重入,之前的 revoke_role_u8token 就踩过这个坑)。

已接入:明日方舟的 player/info(干员/基建/任务/剿灭/保全/公招)、养成+仓库、签到状态与执行。未接入:抽卡记录(走 u8token 的终末地链路,明日方舟不适用)。前端的按游戏分流界面也尚未接入 —— 当前后端只保证数据能正确拉取、解析、缓存与标识。

账户持久化(1457-1538、2628-2810、2812-2847 行)

  • 绑定:save_skland_account 写 cred/token/skland_account_bound/roles;save_selected_roles 写 roles[](含 app / nickname / level / bindingUid)、保留 hytoken / device_token / phone / 已缓存的 user_info、同步 account_list、清理被解绑角色的 role_u8token_*,并写双缓存。
  • 解绑:logout_account 按 roleId 从 roles[] 移除(可选删除 device_token)并清缓存;batch_logout 清 account_list 与 account_token_{id}。
  • 迁移:migrate_roles_app_field 给旧角色补 app 字段(在 save_selected_roles 开头执行);backfill_binding_uid 在拿到绑定列表时按真实游戏回填 app。

懒加载(353-494 行)

set_lazy_load_enabled 在两种模式间转换缓存:开启 → 精简缓存补全 + 清空完整缓存 + retain_only_char_detail(当前角色);关闭 → 用 account_token_* 的 cred/token 合成完整缓存 + preload_all_char_details 预载全部角色。set_current_role_id 把激活角色同步给 NetworkService(影响详情缓存裁剪)。

事件 emit 与定时任务(789-817、975-1016、约 1836 行)

本文件唯一的 emit:notify_cache_refreshed 发送事件 accounts-refreshed,payload 为 AccountRefreshResult { success, error_message, accounts, refresh_time },仅在 refresh_accounts 末尾触发(约 1836)。文件内无任何 listen。start_auto_refresh 以 300 秒周期执行刷新 + 自动签到。

备注

配置键(本文件读写)

  • account_token_{user_id}:核心凭据对象,字段含 cred、token、hytoken、device_token、phone、roles[{userId,serverId,roleId,app,nickname,level,bindingUid,channelMasterId,channelName}]、skland_account_bound、user_info(SklandUserInfo 序列化)。
    • roles[].app 区分游戏:endfield(终末地)/ arknights(明日方舟);缺失时按终末地处理。
    • roles[].nickname / level 是绑定时的快照 —— 明日方舟角色只能靠它们展示(没有终末地详情接口)。
    • ⚠️ 账号级 u8token 已废弃:启动时由 migrate_legacy_u8token 删除;secret_registry 仍登记它只为能清理旧凭据。
    • save_selected_roles 重建该对象时会保留 hytoken / device_token / phone / user_info(后两者以前会被覆盖丢失,已修正),并写入每个角色的 bindingUid / channelMasterId / channelName。
  • role_u8token_{roleId}:已废弃,启动时由 migrate_legacy_u8token 删除(含凭据库条目)。
  • 抽卡授权:不存在独立配置键 —— 它就是 account_token_{user_id}.roles[].u8Authorized(布尔,明文)。u8token 本身从不落盘。
  • skland_games_cache:森空岛游戏列表缓存(常量 SKLAND_GAMES_CACHE_KEY,TTL 12 小时,结构 {fetched_at, games})。
  • lazy_load_enabled:懒加载开关(bool,默认 true)。
  • card_auto_sign_users:开启自动签到的 role_id 列表(只读)。
  • account_list:角色 ID 列表(登出/绑定时增删)。
  • selected_account_id:当前选中账户,由 commands/account.rs 通过 get_config_service() 读写,本文件不涉及(仓库中不存在 selected_account 键)。

HTTP 端点(本文件直接请求,均经 http_client::create_client + capture::send)

  • POST https://as.hypergryph.com/general/v1/send_phone_code(1857)
  • POST https://as.hypergryph.com/user/auth/v2/token_by_phone_code(1911)
  • POST https://as.hypergryph.com/general/v1/gen_scan/login(2011)
  • GET https://as.hypergryph.com/general/v1/scan_status(2074)
  • POST https://as.hypergryph.com/user/auth/v1/token_by_scan_code(2139)
  • POST https://as.hypergryph.com/user/auth/v1/token_by_phone_password(2868)
  • POST https://as.hypergryph.com/user/oauth2/v2/grant(2948)
  • POST https://zonai.skland.com/api/v1/user/auth/generate_cred_by_code(3004)
  • 经 SklandService::call_skland_api:GET/POST /web/v1/game/endfield/attendance(约 879、约 950,基址 https://zonai.skland.com)。
  • 其余森空岛读接口(user/game/binding/role detail/check_cred/refresh/u8token)全部委托 SklandService。

emit / listen 事件

  • emit:accounts-refreshed(789,payload AccountRefreshResult)。
  • listen:无。

调用方(command / 内部)

  • commands/account.rs:get_accounts、get_skland_accounts、save_skland_account、get_skland_account_roles、get_skland_user_info、get_skland_user_info_by_cred、get_skland_games、add_account、logout_account、batch_logout、refresh_accounts、send_verification_code、add_account_by_code、gen_scan_login、scan_status、add_account_by_scan、save_selected_roles、get_selected_account/set_selected_account(仅经 get_config_service)、check_and_refresh_cred、set_lazy_load_enabled、is_lazy_load_enabled、set_current_role_id、query_role_data、authorize_role_u8token / revoke_role_u8token(新增)。
  • commands/attendance.rs:get_attendance/do_attendance 使用 get_config_service、check_and_refresh_user_cred、skland_service、avatar_cache_service。
  • commands/gacha.rs:check_and_refresh_user_cred、fetch_role_u8token(现换令牌)、query_role_data(wiki_catalog)、get_config_service。
  • commands/card_config.rs:仅 get_config_service。
  • 内部定时器 start_auto_refresh:refresh_accounts、get_auto_sign_roles、check_attendance_today、do_attendance_for_role(这三者未暴露为 command)。

发现的异常/注意点

  1. 任务提示中的 selected_account 在仓库中实际为 selected_account_id,且只出现在 commands/account.rs。
  2. batch_logout(约 1595)把入参当作 user_id 拼 account_token_{id} 删除,而 logout_account(约 1490)把同名入参当作 roleId 处理,两者语义不一致,batch_logout 多半删不掉真实凭据键。
  3. set_lazy_load_enabled 内两个分支的锁顺序相反(先完整后精简 / 先精简后完整),并发调用存在理论上的锁顺序风险。
  4. save_selected_roles 中 data.as_object_mut().expect("object checked above") 为可 panic 点;get_accounts 等处 &role_id[..8.min(role_id.len())] 按字节截断,非 ASCII roleId 会 panic。
  5. get_network_service 与访问器 network_service 为死代码,已删除;AsyncAccountService 的 avatar_cache_service 字段亦未使用,已删除。
  6. 渠道服已打通:官服与 B 服共用同一条 u8_token_by_uid 链路(同一个 hytoken / appCode),差异只在 roles[].bindingUid。代价是 appCode be36d44aa36bfb5b 属于鹰角前端硬编码值,存在轮换风险。