feat: WaHappy v0 —— 舞萌DX 纯网络游戏助手(占位版)
当前仅 upsert.py 可用:扫码登录 -> 上传一局成绩 -> 落库回查 -> 登出。 - api/ 下 40+ 个接口模块已实现,236 项零网络自检全绿 - 上传成绩链路已真机跑通,报文与真机抓包逐字节一致 - 开发文档见 docs 分支;source/ 与 logs/ 不入库
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
user_getter_common.py —— 各取数脚本的公共底座
|
||||
================================================================================
|
||||
|
||||
只提供三样东西,取数逻辑都在各自的脚本里:
|
||||
|
||||
1. 各接口的请求 VO(严格对应 source/.../Net.VO.Mai2/*RequestVO.cs)
|
||||
2. _get() 发一次请求并返回**原始响应 dict**
|
||||
3. fetch_all_pages() 自动翻页
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
★ 为什么单独抽出来:各接口的请求体并不统一
|
||||
--------------------------------------------------------------------------------
|
||||
权威依据 source/Assembly-CSharp/Net.VO.Mai2/ 下各自的 XxxRequestVO.cs:
|
||||
|
||||
GameKaleidxScopeRequestVO : 空类,一个字段都没有
|
||||
UserDetailRequestVO : 只有 userId
|
||||
UserMapRequestVO : userId + nextIndex + maxCount = 1000
|
||||
UserLoginBonusRequestVO : userId + nextIndex + maxCount = 20
|
||||
UserItemRequestVO : userId + nextIndex + maxCount = 100
|
||||
UserMusicRequestVO : userId + nextIndex + maxCount = 50
|
||||
UserFavoriteRequestVO : userId + itemKind
|
||||
UserFavoriteItemRequestVO : userId + kind + nextIndex + maxCount = 100 + isAllFavoriteItem
|
||||
UserShopStockRequestVO : userId + shopItemIdList
|
||||
UserRivalMusicRequestVO : userId + rivalId + nextIndex + userRivalMusicLevelList
|
||||
|
||||
发错字段名或漏字段,服务端会返回异常或空数据。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from config import CLIENT_ID, TIMEOUT_MS
|
||||
|
||||
from .log_setup import log_debug, log_warning
|
||||
from .maimai_net import MaimaiNetClient, get_default_client
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 请求 VO(逐字段对应 C# 的 XxxRequestVO)
|
||||
# =============================================================================
|
||||
|
||||
@dataclass
|
||||
class EmptyRequestVO:
|
||||
"""空请求体(对应 GameKaleidxScopeRequestVO —— 它确实没有任何字段)。"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserIdRequestVO:
|
||||
"""只有 userId(对应 UserDetailRequestVO / UserCharacterRequestVO / UserOptionRequestVO 等)。"""
|
||||
userId: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserIdPageRequestVO:
|
||||
"""通用分页(对应 UserMapRequestVO,maxCount 默认 1000)。"""
|
||||
userId: int
|
||||
nextIndex: int = 0
|
||||
maxCount: int = 1000
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserLoginBonusRequestVO:
|
||||
"""对应 UserLoginBonusRequestVO(maxCount 默认 20)。"""
|
||||
userId: int
|
||||
nextIndex: int = 0
|
||||
maxCount: int = 20
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserItemRequestVO:
|
||||
"""对应 UserItemRequestVO(maxCount 默认 100)。"""
|
||||
userId: int
|
||||
nextIndex: int = 0
|
||||
maxCount: int = 100
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserMusicRequestVO:
|
||||
"""对应 UserMusicRequestVO(maxCount 默认 50)。"""
|
||||
userId: int
|
||||
nextIndex: int = 0
|
||||
maxCount: int = 50
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserCourseRequestVO:
|
||||
"""对应 UserCourseRequestVO(只有 userId + nextIndex,**没有** maxCount)。"""
|
||||
userId: int
|
||||
nextIndex: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserFavoriteRequestVO:
|
||||
"""对应 UserFavoriteRequestVO(按 itemKind 分次请求)。"""
|
||||
userId: int
|
||||
itemKind: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserFavoriteItemRequestVO:
|
||||
"""对应 UserFavoriteItemRequestVO(注意字段名是 kind,不是 itemKind)。"""
|
||||
userId: int
|
||||
kind: int = 1
|
||||
nextIndex: int = 0
|
||||
maxCount: int = 100
|
||||
isAllFavoriteItem: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserShopStockRequestVO:
|
||||
"""对应 UserShopStockRequestVO。"""
|
||||
userId: int
|
||||
shopItemIdList: List[int] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserRivalRequestVO:
|
||||
"""对应 UserRivalRequestVO。"""
|
||||
userId: int
|
||||
rivalId: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserRivalMusicRequestVO:
|
||||
"""对应 UserRivalMusicRequestVO(level 列表固定 6 档)。"""
|
||||
userId: int
|
||||
rivalId: int = 0
|
||||
nextIndex: int = 0
|
||||
userRivalMusicLevelList: List[Dict[str, int]] = field(
|
||||
default_factory=lambda: [{"level": lv} for lv in (0, 1, 2, 3, 4, 10)]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 常量
|
||||
# =============================================================================
|
||||
|
||||
#: GetUserItemApi 的 itemKind 取值(对应 Net.VO.Mai2/ItemKind.cs)
|
||||
#: 客户端在 StateUserDownload.cs 里依次下载这些 kind
|
||||
ITEM_KINDS: tuple = (1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 14)
|
||||
|
||||
#: ★ GetUserItemApi 首页 nextIndex 的哨兵系数。
|
||||
#: 依据 source/Assembly-CSharp/Net.Packet.Mai2/PacketGetUserItem.cs:17-24
|
||||
#: Create(GetNetQuery(userId, itemId * 10000000000L));
|
||||
#: 传 0 会被服务端拒绝。真机日志 source/2026-10-05.log:32 也是 10000000000。
|
||||
ITEM_NEXT_INDEX_FACTOR: int = 10000000000
|
||||
|
||||
#: GetUserFavoriteApi 的 itemKind(真机顺序固定 3/1/2/10/11,见 source/2026-10-05.log:59-68)
|
||||
FAVORITE_ITEM_KINDS: tuple = (3, 1, 2, 10, 11)
|
||||
|
||||
#: GetUserRivalMusicApi 的 level 档位(真机 source/2026-10-05.log:101)
|
||||
RIVAL_MUSIC_LEVELS: tuple = (0, 1, 2, 3, 4, 10)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 内部工具
|
||||
# =============================================================================
|
||||
|
||||
def _get(
|
||||
api_name: str,
|
||||
body: Any,
|
||||
user_id: int,
|
||||
*,
|
||||
client_id: str = CLIENT_ID,
|
||||
timeout_ms: Optional[int] = None,
|
||||
client: Optional[MaimaiNetClient] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
发一次请求,返回**原始响应 dict**(不做 VO 映射,保留服务端全部字段)。
|
||||
|
||||
★ 取数脚本一律走这里:upsertUserAll 需要响应原文,
|
||||
映射成 dataclass 会丢掉服务端多给的字段。
|
||||
"""
|
||||
net = client or get_default_client()
|
||||
raw = net.request(
|
||||
api_name, body,
|
||||
user_id=user_id, client_id=client_id,
|
||||
timeout_ms=timeout_ms or TIMEOUT_MS,
|
||||
)
|
||||
if isinstance(raw, str):
|
||||
return json.loads(raw) if raw else {}
|
||||
return raw or {}
|
||||
|
||||
|
||||
def fetch_all_pages(
|
||||
api_name: str,
|
||||
user_id: int,
|
||||
list_key: str,
|
||||
*,
|
||||
body_factory: Optional[Callable[[Optional[int]], Any]] = None,
|
||||
client_id: str = CLIENT_ID,
|
||||
timeout_ms: Optional[int] = None,
|
||||
client: Optional[MaimaiNetClient] = None,
|
||||
max_pages: int = 200,
|
||||
) -> List[Any]:
|
||||
"""
|
||||
自动翻页抓取某个列表接口的全部条目。
|
||||
|
||||
分页约定(各 Packet 的 Proc 里都是这个逻辑):
|
||||
nextIndex == 0 -> 没有更多数据
|
||||
nextIndex != 0 -> 下一页的起始索引
|
||||
|
||||
参数:
|
||||
api_name: 逻辑接口名
|
||||
list_key: 响应里承载列表的键名(如 "userMapList")
|
||||
body_factory: 可选;接收 next_index 返回请求 VO 的工厂。
|
||||
★ 首页会以 next_index=None 调用它 —— 这是为了支持
|
||||
GetUserItemApi 这类「首发要带哨兵值」的接口
|
||||
(首包 nextIndex = itemKind * 1e10,不能是 0)。
|
||||
max_pages: 翻页上限(防御性)
|
||||
|
||||
返回:
|
||||
合并后的全部条目
|
||||
"""
|
||||
all_items: List[Any] = []
|
||||
next_index: Optional[int] = None
|
||||
for page in range(max_pages):
|
||||
if body_factory is not None:
|
||||
body = body_factory(next_index)
|
||||
else:
|
||||
body = UserIdPageRequestVO(userId=user_id, nextIndex=next_index or 0)
|
||||
|
||||
resp = _get(api_name, body, user_id,
|
||||
client_id=client_id, timeout_ms=timeout_ms, client=client)
|
||||
items = resp.get(list_key) or []
|
||||
if isinstance(items, dict):
|
||||
items = [items]
|
||||
all_items.extend(items)
|
||||
|
||||
next_index = int(resp.get("nextIndex") or 0)
|
||||
log_debug(
|
||||
api_name + " 第 " + str(page + 1) + " 页:本页 " + str(len(items))
|
||||
+ " 条,累计 " + str(len(all_items)) + " 条,nextIndex=" + str(next_index)
|
||||
)
|
||||
if not next_index:
|
||||
break
|
||||
else:
|
||||
log_warning(api_name + " 超过 " + str(max_pages) + " 页仍未结束,已中止翻页")
|
||||
|
||||
return all_items
|
||||
Reference in New Issue
Block a user