重构:Get* 命名统一 + 用户数据落盘 + use_ticket / reincarnate
1) 命名统一(服务端 API 名带 Get 的,脚本名与函数名一律带 get/Get)
- 26 个 api 脚本改名:userdata.py -> get_user_data.py 等
- 7 个 game_*.py -> get_game_*.py
- 38 个函数改名:UserData -> GetUserData 等
- upsert_snapshot.py -> get_need_user_data.py,
fetch_upsert_snapshot -> GetNeedUserData
- user_getter_common.fetch_all_pages -> get_all_pages
2) GetNeedUserData 抓到的数据自动落盘
- <工程根>/data/<userId>.json,新数据总是覆盖老数据
- 新增 save_user_data / load_user_data / user_data_path /
list_saved_user_ids / DATA_DIR / PROJECT_ROOT
- data/ 已加入 .gitignore
3) use_ticket.py(新)
- 读取账号当前持有的功能票,用 UpsertUserAll 消耗掉
- 依据:GetUserChargeApi 与 UpsertUserAllApi 的 userChargeList 格式相同;
真机实证 userChargeList.stock -1 + useTicketId + playCredit
- playCredit = 投币数 + 票的 creditNum(VOExtensions.cs:838-845)
4) reincarnate.py(新)
- 账号数据迁移(转生):A 的数据克隆到 B
- 每包最多 4 首(真机一局上限),包间用续关登录换新 loginId
续关依据:ContinueProcess.cs:403 -> PacketUserLogin(isContinue: true)
- playlog 从曲目记录反推合成(没有对应的 Get 接口)
自检 238 项全绿。
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
get_user_preview.py —— 用户预览(GetUserPreviewApi)
|
||||
================================================================================
|
||||
|
||||
功能:刷卡后欢迎界面的数据(昵称、Rating、是否已登录、封禁状态…)。
|
||||
|
||||
对应接口:GetUserPreviewApi
|
||||
请求体 :{"userId":.., "segaIdAuthKey":"", "token":"..", "clientId":".."}
|
||||
source/Assembly-CSharp/Net.VO.Mai2/UserPreviewRequestVO.cs
|
||||
source/Assembly-CSharp/Net.Packet.Mai2/PacketGetUserPreview.cs:20-25
|
||||
响应 :见 UserPreviewResponseVO(23 字段,含 errorId)
|
||||
|
||||
★ 真机调用时机:扫码拿到 userId/token 之后、UserLoginApi 之前
|
||||
(source/2026-10-05.log:23)。
|
||||
★ 客户端行为:isLogin=True 时拒绝继续登录,避免顶号
|
||||
(source/Assembly-CSharp/Process.Entry.State/ConfirmPlay.cs:151)。
|
||||
|
||||
调用示例:
|
||||
from api import GetUserPreview
|
||||
pv = GetUserPreview(12345678, "TESTTOKEN0000000000")
|
||||
print(pv.userName, pv.playerRating)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from config import CLIENT_ID, TIMEOUT_MS
|
||||
|
||||
from .log_setup import log_info, log_warning
|
||||
from .maimai_net import MaimaiNetClient, request_dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserPreviewRequestVO:
|
||||
"""请求 VO(对应 UserPreviewRequestVO)。"""
|
||||
userId: int
|
||||
segaIdAuthKey: str = ""
|
||||
token: str = ""
|
||||
clientId: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserPreviewResponseVO:
|
||||
"""
|
||||
响应 VO(对应 UserPreviewResponseVO + 真机响应字段)。
|
||||
|
||||
★ 字段顺序按 source/Assembly-CSharp/Net.VO.Mai2/UserPreviewResponseVO.cs
|
||||
与 source/2026-10-05.log:24 的真机响应合并。
|
||||
"""
|
||||
userId: int = 0
|
||||
userName: str = ""
|
||||
isLogin: bool = False
|
||||
lastGameId: Optional[str] = None
|
||||
lastRomVersion: str = ""
|
||||
lastDataVersion: str = ""
|
||||
lastLoginDate: str = ""
|
||||
lastPlayDate: str = ""
|
||||
playerRating: int = 0
|
||||
nameplateId: int = 0
|
||||
iconId: int = 0
|
||||
trophyId: int = 0
|
||||
partnerId: int = 0
|
||||
frameId: int = 0
|
||||
dispRate: int = 0
|
||||
totalAwake: int = 0
|
||||
isNetMember: int = 0
|
||||
dailyBonusDate: str = ""
|
||||
headPhoneVolume: Optional[int] = None
|
||||
isInherit: bool = False
|
||||
banState: int = 0
|
||||
errorId: int = 0
|
||||
|
||||
|
||||
def GetUserPreview(
|
||||
user_id: int,
|
||||
token: str,
|
||||
*,
|
||||
client_id: str = CLIENT_ID,
|
||||
sega_id_auth_key: str = "",
|
||||
timeout_ms: Optional[int] = None,
|
||||
client: Optional[MaimaiNetClient] = None,
|
||||
) -> UserPreviewResponseVO:
|
||||
"""
|
||||
获取用户预览信息。
|
||||
|
||||
参数:
|
||||
user_id: 用户 ID(必填)
|
||||
token: 会话令牌(必填,来自 ParseQRCode)
|
||||
client_id: 机台短值(可选)
|
||||
sega_id_auth_key: SEGA ID 鉴权 key(可选,通常为空)
|
||||
timeout_ms: 超时毫秒(可选)
|
||||
client: 可复用的 MaimaiNetClient(可选)
|
||||
|
||||
返回:
|
||||
UserPreviewResponseVO
|
||||
"""
|
||||
log_info("获取用户预览:user_id=" + str(user_id) + " client_id=" + client_id)
|
||||
|
||||
raw = request_dict(
|
||||
"GetUserPreviewApi",
|
||||
UserPreviewRequestVO(
|
||||
userId=user_id,
|
||||
segaIdAuthKey=sega_id_auth_key,
|
||||
token=token,
|
||||
clientId=client_id,
|
||||
),
|
||||
user_id=user_id, client_id=client_id,
|
||||
timeout_ms=timeout_ms or TIMEOUT_MS, client=client,
|
||||
)
|
||||
|
||||
known = {f for f in UserPreviewResponseVO.__dataclass_fields__}
|
||||
resp = UserPreviewResponseVO(**{k: v for k, v in raw.items() if k in known})
|
||||
|
||||
if resp.errorId != 0:
|
||||
log_warning("服务器返回 errorId=" + str(resp.errorId))
|
||||
if resp.banState != 0:
|
||||
log_warning("账号可能被封禁:banState=" + str(resp.banState))
|
||||
if resp.isLogin:
|
||||
log_warning("账号处于已登录状态(isLogin=True),继续登录会顶号")
|
||||
|
||||
log_info(
|
||||
"用户预览获取完成:userName=" + resp.userName
|
||||
+ " playerRating=" + str(resp.playerRating)
|
||||
+ " isLogin=" + str(resp.isLogin)
|
||||
+ " isNetMember=" + str(resp.isNetMember)
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
from .log_setup import log_error, setup_logging, teardown_logging
|
||||
|
||||
def _main() -> int:
|
||||
setup_logging("userpreview_test")
|
||||
try:
|
||||
if len(sys.argv) < 3:
|
||||
print("用法: python -m api.get_user_preview <user_id> <token> [client_id]")
|
||||
return 1
|
||||
cid = sys.argv[3] if len(sys.argv) > 3 else CLIENT_ID
|
||||
pv = GetUserPreview(int(sys.argv[1]), sys.argv[2], client_id=cid)
|
||||
print("")
|
||||
for name in UserPreviewResponseVO.__dataclass_fields__:
|
||||
print(" " + name.ljust(18) + "= " + str(getattr(pv, name)))
|
||||
return 0
|
||||
except Exception as exc:
|
||||
log_error("获取用户预览失败:" + str(exc))
|
||||
return 1
|
||||
finally:
|
||||
teardown_logging()
|
||||
|
||||
raise SystemExit(_main())
|
||||
Reference in New Issue
Block a user