重构: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:
2026-10-06 23:32:09 +08:00
parent c297818d8a
commit 8e3bd6fcaa
46 changed files with 1234 additions and 356 deletions
+153 -136
View File
@@ -25,48 +25,56 @@ api 包 —— 统一导出
user_getter_common.py 取数脚本公共底座(请求 VO / _get / 翻页)
会话链路
chime.py 扫码登录 -> userId + token
gamesettings.py 取游戏设置
ping.py 心跳探活
userpreview.py 用户预览
userlogin.py 正式登录 -> loginId + loginDateTime
userlogout.py 登出
chime.py 扫码登录 -> userId + token
get_game_settings.py GetGameSettingApi(游戏设置)
ping.py 心跳探活
get_user_preview.py GetUserPreviewApi(用户预览)
userlogin.py UserLoginApi(正式登录 -> loginId + loginDateTime)
userlogout.py UserLogoutApi(登出)
上传成绩
upsert_user_all.py ★ 上传成绩(唯一有副作用的接口)
upsert_snapshot.py 结算快照编排(统一调用下面的取数脚本)
上传 / 结算(有副作用的接口)
upsert_user_all.py ★ UpsertUserAllApi(上传成绩)
upsert_user_chargelog.py UpsertUserChargelogApi(购买功能票)
get_need_user_data.py ★ 结算快照编排(统一调用下面的取数脚本),
并把结果落盘到 <工程根>/data/<userId>.json
取数(一个接口一个脚本)
userdata.py GetUserDataApi
user_extend.py GetUserExtendApi
user_option.py GetUserOptionApi
user_character.py GetUserCharacterApi
user_ghost.py GetUserGhostApi
user_map.py GetUserMapApi
user_login_bonus.py GetUserLoginBonusApi
user_rating.py GetUserRatingApi
user_item.py GetUserItemApi
user_music.py GetUserMusicApi
user_course.py GetUserCourseApi
user_charge.py GetUserChargeApi
user_favorite.py GetUserFavoriteApi
user_activity.py GetUserActivityApi
user_mission.py GetUserMissionDataApi
user_intimate.py GetUserIntimateApi
user_shop_stock.py GetUserShopStockApi
user_favorite_item.py GetUserFavoriteItemApi
user_rival.py GetUserRivalDataApi / GetUserRivalMusicApi
user_friend_bonus.py GetUserFriendBonusApi
user_kaleidx_scope.py GetUserKaleidxScopeApi / GetGameKaleidxScopeApi
user_region.py GetUserRegionApi
user_recommend_music.py GetUserRecommendRateMusicApi / GetUserRecommendSelectMusicApi
user_new_item.py GetUserNewItemListApi
game_ranking.py GetGameRankingApi
game_event.py GetGameEventApi
game_charge.py GetGameChargeApi
game_weekly_data.py GetGameWeeklyDataApi
game_tournament_info.py GetGameTournamentInfoApi
game_ng_music_id.py GetGameNgMusicIdApi
★ 命名约定:服务端 API 名里带 Get 的,**脚本名与函数名一律带 get / Get**
(不要模棱两可)。所以下面这些文件全是 get_ 开头。
取数 —— GetUser*(一个接口一个脚本)
get_user_data.py GetUserDataApi
get_user_extend.py GetUserExtendApi
get_user_option.py GetUserOptionApi
get_user_character.py GetUserCharacterApi
get_user_ghost.py GetUserGhostApi
get_user_map.py GetUserMapApi
get_user_login_bonus.py GetUserLoginBonusApi
get_user_rating.py GetUserRatingApi
get_user_item.py GetUserItemApi
get_user_music.py GetUserMusicApi
get_user_course.py GetUserCourseApi
get_user_charge.py GetUserChargeApi
get_user_favorite.py GetUserFavoriteApi
get_user_activity.py GetUserActivityApi
get_user_mission.py GetUserMissionDataApi
get_user_intimate.py GetUserIntimateApi
get_user_shop_stock.py GetUserShopStockApi
get_user_favorite_item.py GetUserFavoriteItemApi
get_user_rival.py GetUserRivalDataApi / GetUserRivalMusicApi
get_user_friend_bonus.py GetUserFriendBonusApi
get_user_kaleidx_scope.py GetUserKaleidxScopeApi / GetGameKaleidxScopeApi
get_user_region.py GetUserRegionApi
get_user_recommend_music.py GetUserRecommendRateMusicApi / GetUserRecommendSelectMusicApi
get_user_new_item.py GetUserNewItemListApi
取数 —— GetGame*
get_game_settings.py GetGameSettingApi
get_game_ranking.py GetGameRankingApi
get_game_event.py GetGameEventApi
get_game_charge.py GetGameChargeApi
get_game_weekly_data.py GetGameWeeklyDataApi
get_game_tournament_info.py GetGameTournamentInfoApi
get_game_ng_music_id.py GetGameNgMusicIdApi
"""
from .log_setup import (
@@ -135,7 +143,7 @@ from .maimai_net import (
from .chime import ParseQRCode
from .gamesettings import (
from .get_game_settings import (
GetGameSettings,
GameSetting,
GameSettingRequestVO,
@@ -146,8 +154,8 @@ from .gamesettings import (
from .ping import Ping
from .userpreview import (
UserPreview,
from .get_user_preview import (
GetUserPreview,
UserPreviewRequestVO,
UserPreviewResponseVO,
)
@@ -230,12 +238,19 @@ from .upsert_user_all import (
USER_MISSION_DROP_KEYS,
)
from .upsert_snapshot import (
fetch_upsert_snapshot,
from .get_need_user_data import (
GetNeedUserData,
save_snapshot,
load_snapshot,
summarize_snapshot,
DEFAULT_SNAPSHOT_NODES,
# ★ 用户数据落盘(<工程根>/data/<userId>.json)
PROJECT_ROOT,
DATA_DIR,
user_data_path,
save_user_data,
load_user_data,
list_saved_user_ids,
)
from .upsert_user_chargelog import (
@@ -269,7 +284,7 @@ from .user_getter_common import (
UserShopStockRequestVO,
UserRivalRequestVO,
UserRivalMusicRequestVO,
fetch_all_pages,
get_all_pages,
ITEM_KINDS,
ITEM_NEXT_INDEX_FACTOR,
FAVORITE_ITEM_KINDS,
@@ -280,42 +295,42 @@ from .user_getter_common import (
# 5) 取数脚本(一个接口一个脚本)
# ---------------------------------------------------------------------------
from .userdata import UserData
from .user_extend import UserExtend
from .user_option import UserOption
from .user_character import UserCharacter
from .user_ghost import UserGhost
from .user_map import UserMap, UserMapAll
from .user_login_bonus import UserLoginBonus, UserLoginBonusAll
from .user_rating import UserRating
from .user_item import UserItem, UserItemAll
from .user_music import UserMusicDetail, UserMusicDetailAll
from .user_course import UserCourse, UserCourseAll
from .user_charge import UserCharge
from .user_favorite import UserFavorite, UserFavoriteAll
from .user_activity import UserActivity
from .user_mission import UserMissionData
from .user_intimate import UserIntimate
from .user_shop_stock import UserShopStock
from .user_favorite_item import (
UserFavoriteItem,
UserFavoriteItemAll,
from .get_user_data import GetUserData
from .get_user_extend import GetUserExtend
from .get_user_option import GetUserOption
from .get_user_character import GetUserCharacter
from .get_user_ghost import GetUserGhost
from .get_user_map import GetUserMap, GetUserMapAll
from .get_user_login_bonus import GetUserLoginBonus, GetUserLoginBonusAll
from .get_user_rating import GetUserRating
from .get_user_item import GetUserItem, GetUserItemAll
from .get_user_music import GetUserMusicDetail, GetUserMusicDetailAll
from .get_user_course import GetUserCourse, GetUserCourseAll
from .get_user_charge import GetUserCharge
from .get_user_favorite import GetUserFavorite, GetUserFavoriteAll
from .get_user_activity import GetUserActivity
from .get_user_mission import GetUserMissionData
from .get_user_intimate import GetUserIntimate
from .get_user_shop_stock import GetUserShopStock
from .get_user_favorite_item import (
GetUserFavoriteItem,
GetUserFavoriteItemAll,
FAVORITE_ITEM_KIND_MUSIC,
FAVORITE_ITEM_KIND_RIVAL,
)
from .user_rival import UserRivalData, UserRivalMusic, UserRivalMusicAll
from .user_friend_bonus import UserFriendBonus, extract_gift_points
from .user_kaleidx_scope import UserKaleidxScope, GameKaleidxScope
from .user_region import UserRegion
from .user_recommend_music import UserRecommendRateMusic, UserRecommendSelectMusic
from .user_new_item import UserNewItemList, UserNewItemListRequestVO
from .get_user_rival import GetUserRivalData, GetUserRivalMusic, GetUserRivalMusicAll
from .get_user_friend_bonus import GetUserFriendBonus, extract_gift_points
from .get_user_kaleidx_scope import GetUserKaleidxScope, GetGameKaleidxScope
from .get_user_region import GetUserRegion
from .get_user_recommend_music import GetUserRecommendRateMusic, GetUserRecommendSelectMusic
from .get_user_new_item import GetUserNewItemList, UserNewItemListRequestVO
from .game_ranking import GetGameRanking, GameRankingRequestVO
from .game_event import GetGameEvent, GameEventRequestVO
from .game_charge import GetGameCharge, GameChargeRequestVO
from .game_weekly_data import GetGameWeeklyData
from .game_tournament_info import GetGameTournamentInfo
from .game_ng_music_id import GetGameNgMusicId
from .get_game_ranking import GetGameRanking, GameRankingRequestVO
from .get_game_event import GetGameEvent, GameEventRequestVO
from .get_game_charge import GetGameCharge, GameChargeRequestVO
from .get_game_weekly_data import GetGameWeeklyData
from .get_game_tournament_info import GetGameTournamentInfo
from .get_game_ng_music_id import GetGameNgMusicId
# ---------------------------------------------------------------------------
# 6) 需要日志跟踪的 api 函数(新增 api 时把函数名加到这里)
@@ -326,7 +341,7 @@ _API_FUNCS = (
"ParseQRCode",
"GetGameSettings",
"Ping",
"UserPreview",
"GetUserPreview",
"UserLogin",
"UserLogout",
# --- 上传成绩 ---
@@ -350,45 +365,45 @@ _API_FUNCS = (
"build_delta",
"build_favorite_delta",
"normalize_snapshot",
"fetch_upsert_snapshot",
"GetNeedUserData",
"summarize_snapshot",
"UserNewItemList",
"GetUserNewItemList",
# --- 取数:单接口 ---
"UserData", # GetUserDataApi
"UserExtend", # GetUserExtendApi
"UserOption", # GetUserOptionApi
"UserCharacter", # GetUserCharacterApi
"UserGhost", # GetUserGhostApi
"UserMap", # GetUserMapApi(单页)
"UserMapAll", # GetUserMapApi(全量)
"UserLoginBonus", # GetUserLoginBonusApi(单页)
"UserLoginBonusAll", # GetUserLoginBonusApi(全量)
"UserRating", # GetUserRatingApi
"UserItem", # GetUserItemApi(单类)
"UserItemAll", # GetUserItemApi(全量)
"UserMusicDetail", # GetUserMusicApi(单页)
"UserMusicDetailAll", # GetUserMusicApi(全量)
"UserCourse", # GetUserCourseApi(单页)
"UserCourseAll", # GetUserCourseApi(全量)
"UserCharge", # GetUserChargeApi
"UserFavorite", # GetUserFavoriteApi(单类)
"UserFavoriteAll", # GetUserFavoriteApi(全量)
"UserActivity", # GetUserActivityApi
"UserMissionData", # GetUserMissionDataApi
"UserIntimate", # GetUserIntimateApi
"UserShopStock", # GetUserShopStockApi
"UserFavoriteItem", # GetUserFavoriteItemApi(单页)
"UserFavoriteItemAll", # GetUserFavoriteItemApi(全量)
"UserRivalData", # GetUserRivalDataApi
"UserRivalMusic", # GetUserRivalMusicApi(单页)
"UserRivalMusicAll", # GetUserRivalMusicApi(全量)
"UserFriendBonus", # GetUserFriendBonusApi
"UserKaleidxScope", # GetUserKaleidxScopeApi
"GameKaleidxScope", # GetGameKaleidxScopeApi
"UserRegion", # GetUserRegionApi
"UserRecommendRateMusic", # GetUserRecommendRateMusicApi
"UserRecommendSelectMusic", # GetUserRecommendSelectMusicApi
"UserNewItemList", # GetUserNewItemListApi
"GetUserData", # GetUserDataApi
"GetUserExtend", # GetUserExtendApi
"GetUserOption", # GetUserOptionApi
"GetUserCharacter", # GetUserCharacterApi
"GetUserGhost", # GetUserGhostApi
"GetUserMap", # GetUserMapApi(单页)
"GetUserMapAll", # GetUserMapApi(全量)
"GetUserLoginBonus", # GetUserLoginBonusApi(单页)
"GetUserLoginBonusAll", # GetUserLoginBonusApi(全量)
"GetUserRating", # GetUserRatingApi
"GetUserItem", # GetUserItemApi(单类)
"GetUserItemAll", # GetUserItemApi(全量)
"GetUserMusicDetail", # GetUserMusicApi(单页)
"GetUserMusicDetailAll", # GetUserMusicApi(全量)
"GetUserCourse", # GetUserCourseApi(单页)
"GetUserCourseAll", # GetUserCourseApi(全量)
"GetUserCharge", # GetUserChargeApi
"GetUserFavorite", # GetUserFavoriteApi(单类)
"GetUserFavoriteAll", # GetUserFavoriteApi(全量)
"GetUserActivity", # GetUserActivityApi
"GetUserMissionData", # GetUserMissionDataApi
"GetUserIntimate", # GetUserIntimateApi
"GetUserShopStock", # GetUserShopStockApi
"GetUserFavoriteItem", # GetUserFavoriteItemApi(单页)
"GetUserFavoriteItemAll", # GetUserFavoriteItemApi(全量)
"GetUserRivalData", # GetUserRivalDataApi
"GetUserRivalMusic", # GetUserRivalMusicApi(单页)
"GetUserRivalMusicAll", # GetUserRivalMusicApi(全量)
"GetUserFriendBonus", # GetUserFriendBonusApi
"GetUserKaleidxScope", # GetUserKaleidxScopeApi
"GetGameKaleidxScope", # GetGameKaleidxScopeApi
"GetUserRegion", # GetUserRegionApi
"GetUserRecommendRateMusic", # GetUserRecommendRateMusicApi
"GetUserRecommendSelectMusic", # GetUserRecommendSelectMusicApi
"GetUserNewItemList", # GetUserNewItemListApi
# --- 取数:游戏侧 ---
"GetGameRanking", # GetGameRankingApi
"GetGameEvent", # GetGameEventApi
@@ -435,7 +450,7 @@ __all__ = [
"GetGameSettings", "GameSetting", "GameSettingRequestVO", "GameSettingResponseVO",
"is_maintenance", "is_ping_disabled",
"Ping",
"UserPreview", "UserPreviewRequestVO", "UserPreviewResponseVO",
"GetUserPreview", "UserPreviewRequestVO", "UserPreviewResponseVO",
"UserLogin", "UserLoginRequestVO", "UserLoginResponseVO",
"UserLogout", "LogoutType", "DelayLog", "RequestAPI",
"UserLogoutRequestVO", "UserLogoutRequest2VO", "UserLogoutResponseVO",
@@ -462,34 +477,36 @@ __all__ = [
"USER_DATA_DROP_KEYS", "USER_OPTION_DROP_KEYS", "USER_CHARACTER_KEEP_KEYS",
"USER_MUSIC_DETAIL_DROP_KEYS", "USER_CHARGE_DROP_KEYS", "USER_MISSION_DROP_KEYS",
# ---- 快照编排 ----
"fetch_upsert_snapshot", "save_snapshot", "load_snapshot",
"GetNeedUserData", "save_snapshot", "load_snapshot",
"summarize_snapshot", "DEFAULT_SNAPSHOT_NODES",
"PROJECT_ROOT", "DATA_DIR", "user_data_path",
"save_user_data", "load_user_data", "list_saved_user_ids",
# ---- 取数公共底座 ----
"GetterEmptyRequestVO", "GetterUserIdRequestVO", "UserIdPageRequestVO",
"UserLoginBonusRequestVO", "UserItemRequestVO", "UserMusicRequestVO",
"UserCourseRequestVO", "UserFavoriteRequestVO", "UserFavoriteItemRequestVO",
"UserShopStockRequestVO", "UserRivalRequestVO", "UserRivalMusicRequestVO",
"fetch_all_pages",
"get_all_pages",
"ITEM_KINDS", "ITEM_NEXT_INDEX_FACTOR", "FAVORITE_ITEM_KINDS", "RIVAL_MUSIC_LEVELS",
# ---- 取数脚本 ----
"UserData", "UserExtend", "UserOption", "UserCharacter", "UserGhost",
"UserMap", "UserMapAll",
"UserLoginBonus", "UserLoginBonusAll",
"UserRating",
"UserItem", "UserItemAll",
"UserMusicDetail", "UserMusicDetailAll",
"UserCourse", "UserCourseAll",
"UserCharge",
"UserFavorite", "UserFavoriteAll",
"UserActivity", "UserMissionData", "UserIntimate", "UserShopStock",
"UserFavoriteItem", "UserFavoriteItemAll",
"GetUserData", "GetUserExtend", "GetUserOption", "GetUserCharacter", "GetUserGhost",
"GetUserMap", "GetUserMapAll",
"GetUserLoginBonus", "GetUserLoginBonusAll",
"GetUserRating",
"GetUserItem", "GetUserItemAll",
"GetUserMusicDetail", "GetUserMusicDetailAll",
"GetUserCourse", "GetUserCourseAll",
"GetUserCharge",
"GetUserFavorite", "GetUserFavoriteAll",
"GetUserActivity", "GetUserMissionData", "GetUserIntimate", "GetUserShopStock",
"GetUserFavoriteItem", "GetUserFavoriteItemAll",
"FAVORITE_ITEM_KIND_MUSIC", "FAVORITE_ITEM_KIND_RIVAL",
"UserRivalData", "UserRivalMusic", "UserRivalMusicAll",
"UserFriendBonus", "extract_gift_points",
"UserKaleidxScope", "GameKaleidxScope",
"UserRegion",
"UserRecommendRateMusic", "UserRecommendSelectMusic",
"UserNewItemList", "UserNewItemListRequestVO",
"GetUserRivalData", "GetUserRivalMusic", "GetUserRivalMusicAll",
"GetUserFriendBonus", "extract_gift_points",
"GetUserKaleidxScope", "GetGameKaleidxScope",
"GetUserRegion",
"GetUserRecommendRateMusic", "GetUserRecommendSelectMusic",
"GetUserNewItemList", "UserNewItemListRequestVO",
"GetGameRanking", "GameRankingRequestVO",
"GetGameEvent", "GameEventRequestVO",
"GetGameCharge", "GameChargeRequestVO",
+1 -1
View File
@@ -235,7 +235,7 @@ def ParseQRCode(qr_input: str, *, chip_id: str = DEFAULT_CHIP_ID) -> Tuple[int,
chip_id: 机台 Keychip 短值(可选,默认取 config.CLIENT_ID)
返回:
(user_id, token) 元组,可直接传给 UserPreview / UserLogin 等
(user_id, token) 元组,可直接传给 GetUserPreview / UserLogin 等
异常:
ValueError: 二维码格式不合法
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
game_charge.py —— 取游戏计费配置(GetGameChargeApi)
get_game_charge.py —— 取游戏计费配置(GetGameChargeApi)
================================================================================
取服务器下发的计费档位。
+1 -1
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
game_event.py —— 取游戏活动(GetGameEventApi)
get_game_event.py —— 取游戏活动(GetGameEventApi)
================================================================================
取服务器下发的活动列表。
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
game_ng_music_id.py —— 取禁用曲目列表(GetGameNgMusicIdApi)
get_game_ng_music_id.py —— 取禁用曲目列表(GetGameNgMusicIdApi)
================================================================================
取服务器下发的 NG(不可游玩)曲目 ID 列表。
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
game_ranking.py —— 取游戏排行(GetGameRankingApi)
get_game_ranking.py —— 取游戏排行(GetGameRankingApi)
================================================================================
取服务器下发的排行榜数据。
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
gamesettings.py —— 获取游戏设置(GetGameSettingApi)
get_game_settings.py —— 获取游戏设置(GetGameSettingApi)
================================================================================
功能:取服务器下发的运行时配置(维护窗口、心跳间隔、包超时、各类上限)。
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
game_tournament_info.py —— 取大会信息(GetGameTournamentInfoApi)
get_game_tournament_info.py —— 取大会信息(GetGameTournamentInfoApi)
================================================================================
取服务器下发的大会/赛事信息。
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
game_weekly_data.py —— 取游戏周常配置(GetGameWeeklyDataApi)
get_game_weekly_data.py —— 取游戏周常配置(GetGameWeeklyDataApi)
================================================================================
取服务器下发的周常周期信息。
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
"""
upsert_snapshot.py —— 结算快照编排(统一调用各取数脚本)
get_need_user_data.py —— 结算快照编排(统一调用各取数脚本)
================================================================================
功能:一键抓取 UpsertUserAll 需要的全部基线数据,整理成「节点字典」。
★ 本模块**只做编排**,取数逻辑都在各自的 api 脚本里(userdata.py / user_map.py …)。
★ 本模块**只做编排**,取数逻辑都在各自的 api 脚本里(get_user_data.py / get_user_map.py …)。
--------------------------------------------------------------------------------
为什么需要快照
@@ -26,15 +26,17 @@ upsert_snapshot.py —— 结算快照编排(统一调用各取数脚本)
--------------------------------------------------------------------------------
调用示例
--------------------------------------------------------------------------------
from api import fetch_upsert_snapshot
from api import GetNeedUserData
snapshot = fetch_upsert_snapshot(user_id, token=token)
snapshot = GetNeedUserData(user_id, token=token)
# 之后直接传给 UpsertUserAll(snapshot=snapshot, ...)
"""
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence
from config import CLIENT_ID, PLACE_ID, PLAY_SHOP_ITEM_IDS, REGION_ID, TIMEOUT_MS
@@ -43,25 +45,103 @@ from .log_setup import log_debug, log_info, log_warning
from .maimai_net import MaimaiNetClient
# 各取数脚本(一个接口一个脚本)
from .userdata import UserData
from .user_extend import UserExtend
from .user_option import UserOption
from .user_character import UserCharacter
from .user_ghost import UserGhost
from .user_map import UserMapAll
from .user_login_bonus import UserLoginBonusAll
from .user_rating import UserRating
from .user_item import UserItemAll
from .user_music import UserMusicDetailAll
from .user_course import UserCourseAll
from .user_charge import UserCharge
from .user_favorite import UserFavoriteAll
from .user_activity import UserActivity
from .user_mission import UserMissionData
from .user_intimate import UserIntimate
from .user_shop_stock import UserShopStock
from .user_favorite_item import UserFavoriteItemAll
from .user_kaleidx_scope import UserKaleidxScope
from .get_user_data import GetUserData
from .get_user_extend import GetUserExtend
from .get_user_option import GetUserOption
from .get_user_character import GetUserCharacter
from .get_user_ghost import GetUserGhost
from .get_user_map import GetUserMapAll
from .get_user_login_bonus import GetUserLoginBonusAll
from .get_user_rating import GetUserRating
from .get_user_item import GetUserItemAll
from .get_user_music import GetUserMusicDetailAll
from .get_user_course import GetUserCourseAll
from .get_user_charge import GetUserCharge
from .get_user_favorite import GetUserFavoriteAll
from .get_user_activity import GetUserActivity
from .get_user_mission import GetUserMissionData
from .get_user_intimate import GetUserIntimate
from .get_user_shop_stock import GetUserShopStock
from .get_user_favorite_item import GetUserFavoriteItemAll
from .get_user_kaleidx_scope import GetUserKaleidxScope
# =============================================================================
# 落盘目录:<工程根>/data/<userId>.json
# =============================================================================
#: 工程根目录(本文件在 <root>/api/ 下)
PROJECT_ROOT: Path = Path(__file__).resolve().parent.parent
#: 用户数据目录:<工程根>/data
DATA_DIR: Path = PROJECT_ROOT / "data"
def user_data_path(user_id: int) -> Path:
"""用户数据的落盘路径:<工程根>/data/<userId>.json"""
return DATA_DIR / (str(int(user_id)) + ".json")
def save_user_data(snapshot: Dict[str, Any], user_id: int,
path: Optional[str] = None) -> str:
"""
把 GetNeedUserData 抓到的数据写到 <工程根>/data/<userId>.json。
★ **新数据总是覆盖老数据**(不是追加、也不是合并)。
★ 目录不存在会自动创建。
参数:
snapshot: GetNeedUserData 的返回值
user_id: 用户 ID(决定文件名)
path: 自定义路径(可选,默认走 data/<userId>.json)
返回:实际写入的文件路径
"""
target = Path(path) if path else user_data_path(user_id)
try:
os.makedirs(str(target.parent), exist_ok=True)
with open(str(target), "w", encoding="utf-8") as fp:
json.dump(snapshot, fp, ensure_ascii=False, indent=2, default=str)
except Exception as exc:
log_warning("用户数据落盘失败(不影响本次运行):" + str(exc))
return str(target)
size = 0
try:
size = os.path.getsize(str(target))
except Exception:
pass
log_info("用户数据已保存(覆盖):" + str(target) + " " + str(size) + " 字节")
return str(target)
def load_user_data(user_id: int, path: Optional[str] = None) -> Dict[str, Any]:
"""
读回 <工程根>/data/<userId>.json。
找不到文件时抛 FileNotFoundError(调用方自行决定要不要回退到联网抓取)。
"""
target = Path(path) if path else user_data_path(user_id)
if not target.exists():
raise FileNotFoundError("用户数据文件不存在:" + str(target))
with open(str(target), "r", encoding="utf-8") as fp:
data = json.load(fp)
log_info("用户数据已加载:" + str(target))
return data
def list_saved_user_ids() -> List[int]:
"""列出 data/ 下已保存的全部 userId(按文件名解析,跳过解析失败的)。"""
if not DATA_DIR.exists():
return []
out: List[int] = []
for name in os.listdir(str(DATA_DIR)):
if not name.endswith(".json"):
continue
try:
out.append(int(name[:-5]))
except ValueError:
continue
return sorted(out)
#: 默认要抓的节点(顺序即抓取顺序)
@@ -89,7 +169,7 @@ DEFAULT_SNAPSHOT_NODES: tuple = (
)
def fetch_upsert_snapshot(
def GetNeedUserData(
user_id: int,
*,
token: str = "",
@@ -101,6 +181,8 @@ def fetch_upsert_snapshot(
timeout_ms: Optional[int] = None,
client: Optional[MaimaiNetClient] = None,
strict: bool = False,
save: bool = True,
save_path: Optional[str] = None,
) -> Dict[str, Any]:
"""
抓取结算所需的全部基线数据。
@@ -117,6 +199,9 @@ def fetch_upsert_snapshot(
client: 可复用的 MaimaiNetClient
strict: True = 任一接口失败就抛异常;
False(默认)= 记录失败并跳过,保证链路不崩
save: True(默认)= 抓完自动落盘到 <工程根>/data/<userId>.json
(★ 新数据总是覆盖老数据)
save_path: 自定义落盘路径(可选,给了就忽略 save 的默认路径规则)
返回:
{
@@ -151,78 +236,87 @@ def fetch_upsert_snapshot(
# ---- 逐个抓取(顺序与 DEFAULT_SNAPSHOT_NODES 一致)----
if "userData" in wanted:
_call("userData", UserData, user_id, **common)
_call("userData", GetUserData, user_id, **common)
if "userExtend" in wanted:
_call("userExtend", UserExtend, user_id, **common)
_call("userExtend", GetUserExtend, user_id, **common)
if "userOption" in wanted:
_call("userOption", UserOption, user_id, **common)
_call("userOption", GetUserOption, user_id, **common)
if "userCharacterList" in wanted:
_call("userCharacterList", UserCharacter, user_id, **common)
_call("userCharacterList", GetUserCharacter, user_id, **common)
if "userMapList" in wanted:
_call("userMapList", UserMapAll, user_id, **common)
_call("userMapList", GetUserMapAll, user_id, **common)
if "userLoginBonusList" in wanted:
_call("userLoginBonusList", UserLoginBonusAll, user_id, **common)
_call("userLoginBonusList", GetUserLoginBonusAll, user_id, **common)
if "userRatingList" in wanted:
_call("userRatingList", UserRating, user_id, **common)
_call("userRatingList", GetUserRating, user_id, **common)
if "userItemList" in wanted:
_call("userItemList", UserItemAll, user_id, **common)
_call("userItemList", GetUserItemAll, user_id, **common)
if "userMusicDetailList" in wanted:
_call("userMusicDetailList", UserMusicDetailAll, user_id, **common)
_call("userMusicDetailList", GetUserMusicDetailAll, user_id, **common)
if "userCourseList" in wanted:
_call("userCourseList", UserCourseAll, user_id, **common)
_call("userCourseList", GetUserCourseAll, user_id, **common)
if "userChargeList" in wanted:
_call("userChargeList", UserCharge, user_id, **common)
_call("userChargeList", GetUserCharge, user_id, **common)
if "userFavoriteList" in wanted:
# ★ 该接口额外带一份**原始** itemKind 副本(_raw),
# 供 UpsertUserAll 生成 isNewFavoriteList 位串用
# —— 真机 itemKind=10 回的是 null,正是它产生了位串里的 "1"。
if _call("userFavoriteList", UserFavoriteAll, user_id, **common):
if _call("userFavoriteList", GetUserFavoriteAll, user_id, **common):
fav_raw = (raw.get("userFavoriteList") or {}).get("_raw")
if fav_raw:
raw["_raw_favorite"] = fav_raw
if "userActivityList" in wanted:
_call("userActivityList", UserActivity, user_id, **common)
_call("userActivityList", GetUserActivity, user_id, **common)
if "userMissionDataList" in wanted:
# ★ 该接口的响应里同时带着 userWeeklyData(周界),
# 它是 Upsert 侧 userWeeklyData 节点的来源,会被自动提取。
_call("userMissionDataList", UserMissionData, user_id, **common)
_call("userMissionDataList", GetUserMissionData, user_id, **common)
if "userIntimateList" in wanted:
_call("userIntimateList", UserIntimate, user_id, **common)
_call("userIntimateList", GetUserIntimate, user_id, **common)
if "userShopItemStockList" in wanted:
ids = list(shop_item_ids) if shop_item_ids is not None else list(PLAY_SHOP_ITEM_IDS)
if ids:
_call("userShopItemStockList", UserShopStock, user_id,
_call("userShopItemStockList", GetUserShopStock, user_id,
shop_item_id_list=ids, **common)
else:
log_debug("未配置 PLAY_SHOP_ITEM_IDS,跳过商店库存节点")
if "userFavoritemusicList" in wanted:
_call("userFavoritemusicList", UserFavoriteItemAll, user_id, 1, **common)
_call("userFavoritemusicList", GetUserFavoriteItemAll, user_id, 1, **common)
if "userKaleidxScopeList" in wanted:
_call("userKaleidxScopeList", UserKaleidxScope, user_id, **common)
_call("userKaleidxScopeList", GetUserKaleidxScope, user_id, **common)
if "userGhost" in wanted:
_call("userGhost", UserGhost, user_id, **common)
_call("userGhost", GetUserGhost, user_id, **common)
raw["_failed"] = failed
log_info("结算快照抓取完成:成功 " + str(len(raw) - 1) + " 个节点,失败 "
raw["_userId"] = int(user_id)
log_info("结算快照抓取完成:成功 " + str(len(raw) - 2) + " 个节点,失败 "
+ str(len(failed)) + " 个")
# ★ 落盘:<工程根>/data/<userId>.json(覆盖式)
if save or save_path:
try:
save_user_data(raw, user_id, path=save_path)
except Exception as exc:
log_warning("用户数据落盘失败(不影响本次运行):" + str(exc))
return raw
@@ -281,10 +375,10 @@ if __name__ == "__main__":
from .log_setup import log_error, setup_logging, teardown_logging
def _main() -> int:
setup_logging("upsert_snapshot_test")
setup_logging("get_need_user_data_test")
try:
if len(sys.argv) < 2:
print("用法: python -m api.upsert_snapshot <user_id> [--token TOKEN] [--save 路径]")
print("用法: python -m api.get_need_user_data <user_id> [--token TOKEN] [--save 路径]")
return 1
user_id = int(sys.argv[1])
@@ -302,7 +396,7 @@ if __name__ == "__main__":
else:
i += 1
snap = fetch_upsert_snapshot(user_id, token=token)
snap = GetNeedUserData(user_id, token=token)
summary = summarize_snapshot(snap)
print("")
for node, count in summary.items():
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_activity.py —— 取用户活动记录(GetUserActivityApi)
get_user_activity.py —— 取用户活动记录(GetUserActivityApi)
================================================================================
取玩家的活动/成就记录(playList 与 musicList 两个列表)。
@@ -16,7 +16,7 @@ user_activity.py —— 取用户活动记录(GetUserActivityApi)
不是把两个数组首尾相接摊平。
调用示例:
from api import UserActivity
from api import GetUserActivity
"""
from __future__ import annotations
@@ -30,7 +30,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserActivity(
def GetUserActivity(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_character.py —— 取用户角色列表(GetUserCharacterApi)
get_user_character.py —— 取用户角色列表(GetUserCharacterApi)
================================================================================
取玩家所有角色的等级与觉醒度(真机 312 条)。
@@ -14,7 +14,7 @@ user_character.py —— 取用户角色列表(GetUserCharacterApi)
(真机 source/2026-10-05.log:123 的 userCharacterList 就是这 4 个键)。
调用示例:
from api import UserCharacter
from api import GetUserCharacter
"""
from __future__ import annotations
@@ -28,7 +28,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserCharacter(
def GetUserCharacter(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_charge.py —— 取用户票券(GetUserChargeApi)
get_user_charge.py —— 取用户票券(GetUserChargeApi)
================================================================================
取玩家的票券持有情况(真机 3 条)。
@@ -13,7 +13,7 @@ user_charge.py —— 取用户票券(GetUserChargeApi)
★ 回传 Upsert 时要丢掉 extNum1(真机 Upsert 报文里没有这个字段)。
调用示例:
from api import UserCharge
from api import GetUserCharge
"""
from __future__ import annotations
@@ -27,7 +27,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserCharge(
def GetUserCharge(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_course.py —— 取用户课题模式成绩(GetUserCourseApi)
get_user_course.py —— 取用户课题模式成绩(GetUserCourseApi)
================================================================================
取玩家的 COURSE(课题)模式通关记录(真机 16 条)。
@@ -12,7 +12,7 @@ user_course.py —— 取用户课题模式成绩(GetUserCourseApi)
source/2026-10-05.log:55 请求体就是 {"userId":..,"nextIndex":0}
调用示例:
from api import UserCourse
from api import GetUserCourse
"""
from __future__ import annotations
@@ -23,10 +23,10 @@ from config import CLIENT_ID, TIMEOUT_MS
from .log_setup import log_info
from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserCourseRequestVO, fetch_all_pages
from .user_getter_common import _get, UserCourseRequestVO, get_all_pages
def UserCourse(
def GetUserCourse(
user_id: int,
*,
next_index: int = 0,
@@ -48,7 +48,7 @@ def UserCourse(
)
def UserCourseAll(
def GetUserCourseAll(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -62,7 +62,7 @@ def UserCourseAll(
{"userCourseList": [...]}
"""
log_info("取用户课题成绩(全量):user_id=" + str(user_id))
items = fetch_all_pages(
items = get_all_pages(
"GetUserCourseApi", user_id, "userCourseList",
body_factory=lambda ni: UserCourseRequestVO(userId=user_id, nextIndex=ni or 0),
client_id=client_id, timeout_ms=timeout_ms, client=client,
+3 -3
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
userdata.py —— 取用户全量数据(GetUserDataApi)
get_user_data.py —— 取用户全量数据(GetUserDataApi)
================================================================================
取玩家档案(86 个字段的大对象),是 upsertUserAll.userData 的**唯一正确来源**。
@@ -20,7 +20,7 @@ userdata.py —— 取用户全量数据(GetUserDataApi)
记录造成的假象(两条响应被合并显示)。
调用示例:
from api import UserData
from api import GetUserData
"""
from __future__ import annotations
@@ -34,7 +34,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserData(
def GetUserData(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_extend.py —— 取用户扩展设置(GetUserExtendApi)
get_user_extend.py —— 取用户扩展设置(GetUserExtendApi)
================================================================================
取玩家上次的选曲 / 分类 / 卡片槽等扩展状态。
@@ -14,7 +14,7 @@ user_extend.py —— 取用户扩展设置(GetUserExtendApi)
(真机两处都是 2022),组包时要注意。
调用示例:
from api import UserExtend
from api import GetUserExtend
"""
from __future__ import annotations
@@ -28,7 +28,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserExtend(
def GetUserExtend(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_favorite.py —— 取用户收藏(GetUserFavoriteApi)
get_user_favorite.py —— 取用户收藏(GetUserFavoriteApi)
================================================================================
取玩家各类收藏夹(称号 / 头像 / 相框 / 搭档 / 背景…)。
@@ -15,7 +15,7 @@ user_favorite.py —— 取用户收藏(GetUserFavoriteApi)
位串 isNewFavoriteList 是三值 "0"/"1"/"2"。
调用示例:
from api import UserFavorite, UserFavoriteAll
from api import GetUserFavorite, GetUserFavoriteAll
"""
from __future__ import annotations
@@ -29,7 +29,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserFavoriteRequestVO, FAVORITE_ITEM_KINDS
def UserFavorite(
def GetUserFavorite(
user_id: int,
item_kind: int,
*,
@@ -55,7 +55,7 @@ def UserFavorite(
)
def UserFavoriteAll(
def GetUserFavoriteAll(
user_id: int,
*,
item_kinds: Optional[List[int]] = None,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_favorite_item.py —— 取用户收藏曲目/对手(GetUserFavoriteItemApi)
get_user_favorite_item.py —— 取用户收藏曲目/对手(GetUserFavoriteItemApi)
================================================================================
取玩家收藏的乐曲(kind=1)或收藏的对手(kind=2)。
@@ -15,7 +15,7 @@ user_favorite_item.py —— 取用户收藏曲目/对手(GetUserFavoriteItemA
★ 字段名是 kind(不是 itemKind),别写错。
调用示例:
from api import UserFavoriteItem, UserFavoriteItemAll
from api import GetUserFavoriteItem, GetUserFavoriteItemAll
"""
from __future__ import annotations
@@ -26,14 +26,14 @@ from config import CLIENT_ID, TIMEOUT_MS
from .log_setup import log_info
from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserFavoriteItemRequestVO, fetch_all_pages
from .user_getter_common import _get, UserFavoriteItemRequestVO, get_all_pages
#: kind 取值
FAVORITE_ITEM_KIND_MUSIC = 1 # 收藏乐曲
FAVORITE_ITEM_KIND_RIVAL = 2 # 收藏对手
def UserFavoriteItem(
def GetUserFavoriteItem(
user_id: int,
kind: int = FAVORITE_ITEM_KIND_MUSIC,
*,
@@ -65,7 +65,7 @@ def UserFavoriteItem(
)
def UserFavoriteItemAll(
def GetUserFavoriteItemAll(
user_id: int,
kind: int = FAVORITE_ITEM_KIND_MUSIC,
*,
@@ -81,7 +81,7 @@ def UserFavoriteItemAll(
{"userFavoriteItemList": [...]}
"""
log_info("取用户收藏项(全量):user_id=" + str(user_id) + " kind=" + str(kind))
items = fetch_all_pages(
items = get_all_pages(
"GetUserFavoriteItemApi", user_id, "userFavoriteItemList",
body_factory=lambda ni: UserFavoriteItemRequestVO(
userId=user_id, kind=kind, nextIndex=ni or 0,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_friend_bonus.py —— 取用户好友里程(GetUserFriendBonusApi)
get_user_friend_bonus.py —— 取用户好友里程(GetUserFriendBonusApi)
================================================================================
取玩家本次可领取的好友里程(getMiles)。
@@ -12,7 +12,7 @@ user_friend_bonus.py —— 取用户好友里程(GetUserFriendBonusApi)
source/2026-10-05.log:111 真机响应 {"returnCode":1,"userId":..,"getMiles":90}
调用示例:
from api import UserFriendBonus, extract_gift_points
from api import GetUserFriendBonus, extract_gift_points
"""
from __future__ import annotations
@@ -26,7 +26,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserFriendBonus(
def GetUserFriendBonus(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -53,7 +53,7 @@ def extract_gift_points(friend_bonus: Dict[str, Any]) -> int:
从好友里程响应里取出 getMiles(本次可领取的里程数)。
参数:
friend_bonus: UserFriendBonus 的返回值
friend_bonus: GetUserFriendBonus 的返回值
返回:
getMiles 整数(取不到则为 0)
+4 -4
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_ghost.py —— 取用户幽灵数据(GetUserGhostApi)
get_user_ghost.py —— 取用户幽灵数据(GetUserGhostApi)
================================================================================
取玩家的「幽灵」对战记录(真机 15 条)。
@@ -11,11 +11,11 @@ user_ghost.py —— 取用户幽灵数据(GetUserGhostApi)
依据 :source/Assembly-CSharp/Net.VO.Mai2/UserGhostRequestVO.cs
★ Upsert 侧 userGhost 默认留空:源码 VOExtensions.cs:235 是
dst.userGhost = (!src.MyGhost.NeedSave) ? null : new UserGhost[1] {...};
dst.userGhost = (!src.MyGhost.NeedSave) ? null : new GetUserGhost[1] {...};
只有「本局产生了幽灵」才回传,纯网络脚本复刻不出幽灵数据。
调用示例:
from api import UserGhost
from api import GetUserGhost
"""
from __future__ import annotations
@@ -29,7 +29,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserGhost(
def GetUserGhost(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_intimate.py —— 取用户搭档亲密度(GetUserIntimateApi)
get_user_intimate.py —— 取用户搭档亲密度(GetUserIntimateApi)
================================================================================
取玩家与各搭档的亲密度(真机 2 条)。
@@ -11,7 +11,7 @@ user_intimate.py —— 取用户搭档亲密度(GetUserIntimateApi)
依据 :source/Assembly-CSharp/Net.VO.Mai2/UserIntimateRequestVO.cs
调用示例:
from api import UserIntimate
from api import GetUserIntimate
"""
from __future__ import annotations
@@ -25,7 +25,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserIntimate(
def GetUserIntimate(
user_id: int,
*,
client_id: str = CLIENT_ID,
+4 -4
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_item.py —— 取用户道具(GetUserItemApi)
get_user_item.py —— 取用户道具(GetUserItemApi)
================================================================================
取玩家各类道具的库存(真机按 kind 分 12 次请求)。
@@ -29,7 +29,7 @@ source/Assembly-CSharp/Net.Packet.Mai2/PacketGetUserItem.cs:17-24
后续翻页沿用响应回的 nextIndex
调用示例:
from api import UserItem, UserItemAll
from api import GetUserItem, GetUserItemAll
"""
from __future__ import annotations
@@ -43,7 +43,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserItemRequestVO, ITEM_KINDS, ITEM_NEXT_INDEX_FACTOR
def UserItem(
def GetUserItem(
user_id: int,
item_kind: int,
*,
@@ -77,7 +77,7 @@ def UserItem(
)
def UserItemAll(
def GetUserItemAll(
user_id: int,
*,
item_kinds: Optional[List[int]] = None,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_kaleidx_scope.py —— 取 KALEIDX 门进度(GetUserKaleidxScopeApi / GetGameKaleidxScopeApi)
get_user_kaleidx_scope.py —— 取 KALEIDX 门进度(GetUserKaleidxScopeApi / GetGameKaleidxScopeApi)
================================================================================
对应接口:
@@ -17,14 +17,14 @@ user_kaleidx_scope.py —— 取 KALEIDX 门进度(GetUserKaleidxScopeApi / Ge
★★ 重要区分(曾导致 userData 取空)
--------------------------------------------------------------------------------
GameKaleidxScopeResponseVO 里**只有** gameKaleidxScopeList 字段,没有 userData。
玩家档案 userData 的正确来源是 GetUserDataApi(见 userdata.py)。
玩家档案 userData 的正确来源是 GetUserDataApi(见 get_user_data.py)。
真机日志里「GetGameKaleidxScope 的响应带 userData」是抓包工具把时间相邻的
两条响应合并显示造成的假象 —— 不要据此给 GetGameKaleidxScopeApi 发 userId,
多发字段会让服务端返回空内容。
调用示例:
from api import UserKaleidxScope, GameKaleidxScope
from api import GetUserKaleidxScope, GetGameKaleidxScope
"""
from __future__ import annotations
@@ -38,7 +38,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, EmptyRequestVO, UserIdRequestVO
def UserKaleidxScope(
def GetUserKaleidxScope(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -56,7 +56,7 @@ def UserKaleidxScope(
client_id=client_id, timeout_ms=timeout_ms, client=client)
def GameKaleidxScope(
def GetGameKaleidxScope(
*,
client_id: str = CLIENT_ID,
timeout_ms: Optional[int] = None,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_login_bonus.py —— 取用户登录奖励(GetUserLoginBonusApi)
get_user_login_bonus.py —— 取用户登录奖励(GetUserLoginBonusApi)
================================================================================
取玩家的登录奖励领取进度(真机 38 条,分两页)。
@@ -13,7 +13,7 @@ user_login_bonus.py —— 取用户登录奖励(GetUserLoginBonusApi)
★ 回传 Upsert 时只回传 isCurrent=true 的那一条(本次签到的那条)。
调用示例:
from api import UserLoginBonus, UserLoginBonusAll
from api import GetUserLoginBonus, GetUserLoginBonusAll
"""
from __future__ import annotations
@@ -24,10 +24,10 @@ from config import CLIENT_ID, TIMEOUT_MS
from .log_setup import log_info
from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserLoginBonusRequestVO, fetch_all_pages
from .user_getter_common import _get, UserLoginBonusRequestVO, get_all_pages
def UserLoginBonus(
def GetUserLoginBonus(
user_id: int,
*,
next_index: int = 0,
@@ -52,7 +52,7 @@ def UserLoginBonus(
)
def UserLoginBonusAll(
def GetUserLoginBonusAll(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -66,7 +66,7 @@ def UserLoginBonusAll(
{"userLoginBonusList": [...]}
"""
log_info("取用户登录奖励(全量):user_id=" + str(user_id))
items = fetch_all_pages(
items = get_all_pages(
"GetUserLoginBonusApi", user_id, "userLoginBonusList",
body_factory=lambda ni: UserLoginBonusRequestVO(
userId=user_id, nextIndex=ni or 0, maxCount=20),
+6 -6
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_map.py —— 取用户地图进度(GetUserMapApi)
get_user_map.py —— 取用户地图进度(GetUserMapApi)
================================================================================
取玩家各地图的推进距离(真机 104 条)。
@@ -14,7 +14,7 @@ user_map.py —— 取用户地图进度(GetUserMapApi)
(真机:mapId=550003 的 distance 1424000 -> 1435000)。
调用示例:
from api import UserMap / UserMapAll
from api import GetUserMap / GetUserMapAll
"""
from __future__ import annotations
@@ -25,10 +25,10 @@ from config import CLIENT_ID, TIMEOUT_MS
from .log_setup import log_info
from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdPageRequestVO, fetch_all_pages
from .user_getter_common import _get, UserIdPageRequestVO, get_all_pages
def UserMap(
def GetUserMap(
user_id: int,
*,
next_index: int = 0,
@@ -53,7 +53,7 @@ def UserMap(
)
def UserMapAll(
def GetUserMapAll(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -67,7 +67,7 @@ def UserMapAll(
{"userMapList": [...]}
"""
log_info("取用户地图进度(全量):user_id=" + str(user_id))
items = fetch_all_pages(
items = get_all_pages(
"GetUserMapApi", user_id, "userMapList",
body_factory=lambda ni: UserIdPageRequestVO(
userId=user_id, nextIndex=ni or 0, maxCount=1000),
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_mission.py —— 取用户任务与周数据(GetUserMissionDataApi)
get_user_mission.py —— 取用户任务与周数据(GetUserMissionDataApi)
================================================================================
取玩家的任务进度与周常数据(真机 6 条任务)。
@@ -16,7 +16,7 @@ user_mission.py —— 取用户任务与周数据(GetUserMissionDataApi)
★ 两者回传 Upsert 时都要丢掉各自的 userId 字段。
调用示例:
from api import UserMissionData
from api import GetUserMissionData
"""
from __future__ import annotations
@@ -30,7 +30,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserMissionData(
def GetUserMissionData(
user_id: int,
*,
client_id: str = CLIENT_ID,
+7 -7
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_music.py —— 取用户曲目成绩(GetUserMusicApi)
get_user_music.py —— 取用户曲目成绩(GetUserMusicApi)
================================================================================
取玩家全部曲目的历史成绩归档(真机 739 组)。
@@ -10,7 +10,7 @@ user_music.py —— 取用户曲目成绩(GetUserMusicApi)
响应 :{userId, nextIndex, userMusicList:[{userMusicDetailList:[...], length}]}
依据 :source/Assembly-CSharp/Net.VO.Mai2/UserMusicRequestVO.cs
source/Assembly-CSharp/Net.VO.Mai2/UserMusic.cs(外层按曲分组)
source/Assembly-CSharp/Net.VO.Mai2/UserMusicDetail.cs(内层按难度)
source/Assembly-CSharp/Net.VO.Mai2/GetUserMusicDetail.cs(内层按难度)
★ 响应是**两层嵌套**:外层每元素是一首歌,内层的 userMusicDetailList 才是
按 (musicId, level) 的成绩条目。取数时要摊平。
@@ -18,7 +18,7 @@ user_music.py —— 取用户曲目成绩(GetUserMusicApi)
★ 回传 Upsert 时只回传本局玩的那一条(playCount +1),其余不动。
调用示例:
from api import UserMusicDetail, UserMusicDetailFlat
from api import GetUserMusicDetail, UserMusicDetailFlat
"""
from __future__ import annotations
@@ -29,10 +29,10 @@ from config import CLIENT_ID, TIMEOUT_MS
from .log_setup import log_info
from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserMusicRequestVO, fetch_all_pages
from .user_getter_common import _get, UserMusicRequestVO, get_all_pages
def UserMusicDetail(
def GetUserMusicDetail(
user_id: int,
*,
next_index: int = 0,
@@ -57,7 +57,7 @@ def UserMusicDetail(
)
def UserMusicDetailAll(
def GetUserMusicDetailAll(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -72,7 +72,7 @@ def UserMusicDetailAll(
每元素是**单条 (musicId, level) 成绩**,不是按曲分组的嵌套结构。
"""
log_info("取用户曲目成绩(全量):user_id=" + str(user_id))
groups = fetch_all_pages(
groups = get_all_pages(
"GetUserMusicApi", user_id, "userMusicList",
body_factory=lambda ni: UserMusicRequestVO(
userId=user_id, nextIndex=ni or 0, maxCount=50),
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_new_item.py —— 取新增道具(GetUserNewItemListApi)
get_user_new_item.py —— 取新增道具(GetUserNewItemListApi)
================================================================================
上传结算后查询「本次新增了哪些道具」(真机响应 userItemList 为空)。
@@ -15,7 +15,7 @@ user_new_item.py —— 取新增道具(GetUserNewItemListApi)
★ 请求体里的 userData / userPlaylogList 需要传,内容与 UpsertUserAll 一致。
调用示例:
from api import UserNewItemList
from api import GetUserNewItemList
"""
from __future__ import annotations
@@ -38,7 +38,7 @@ class UserNewItemListRequestVO:
userPlaylogList: List[Any] = field(default_factory=list)
def UserNewItemList(
def GetUserNewItemList(
user_id: int,
*,
version: int = PLAY_VERSION_INT,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_option.py —— 取用户游戏设置(GetUserOptionApi)
get_user_option.py —— 取用户游戏设置(GetUserOptionApi)
================================================================================
取玩家的音符速度、判定显示、音量等游戏内设置(48 个字段)。
@@ -15,7 +15,7 @@ user_option.py —— 取用户游戏设置(GetUserOptionApi)
裁剪时要丢掉 tempoVolume(真机 Upsert 报文里没有这个字段)。
调用示例:
from api import UserOption
from api import GetUserOption
"""
from __future__ import annotations
@@ -29,7 +29,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserOption(
def GetUserOption(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
userpreview.py —— 用户预览(GetUserPreviewApi)
get_user_preview.py —— 用户预览(GetUserPreviewApi)
================================================================================
功能:刷卡后欢迎界面的数据(昵称、Rating、是否已登录、封禁状态…)。
@@ -17,8 +17,8 @@ userpreview.py —— 用户预览(GetUserPreviewApi)
(source/Assembly-CSharp/Process.Entry.State/ConfirmPlay.cs:151)。
调用示例:
from api import UserPreview
pv = UserPreview(12345678, "TESTTOKEN0000000000")
from api import GetUserPreview
pv = GetUserPreview(12345678, "TESTTOKEN0000000000")
print(pv.userName, pv.playerRating)
"""
@@ -74,7 +74,7 @@ class UserPreviewResponseVO:
errorId: int = 0
def UserPreview(
def GetUserPreview(
user_id: int,
token: str,
*,
@@ -139,10 +139,10 @@ if __name__ == "__main__":
setup_logging("userpreview_test")
try:
if len(sys.argv) < 3:
print("用法: python -m api.userpreview <user_id> <token> [client_id]")
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 = UserPreview(int(sys.argv[1]), sys.argv[2], client_id=cid)
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)))
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_rating.py —— 取用户 Rating 明细(GetUserRatingApi)
get_user_rating.py —— 取用户 Rating 明细(GetUserRatingApi)
================================================================================
取玩家的 Rating 构成(旧谱 / 新谱 / 下一档候选 + 段位)。
@@ -14,7 +14,7 @@ user_rating.py —— 取用户 Rating 明细(GetUserRatingApi)
★ 回传 Upsert 时是**单元素数组** userRatingList:[{...}],不是对象。
调用示例:
from api import UserRating
from api import GetUserRating
"""
from __future__ import annotations
@@ -28,7 +28,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserRating(
def GetUserRating(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_recommend_music.py —— 取推荐曲目(GetUserRecommendRateMusicApi / GetUserRecommendSelectMusicApi)
get_user_recommend_music.py —— 取推荐曲目(GetUserRecommendRateMusicApi / GetUserRecommendSelectMusicApi)
================================================================================
对应接口:
@@ -14,7 +14,7 @@ user_recommend_music.py —— 取推荐曲目(GetUserRecommendRateMusicApi /
source/2026-10-05.log:79,81
调用示例:
from api import UserRecommendRateMusic, UserRecommendSelectMusic
from api import GetUserRecommendRateMusic, GetUserRecommendSelectMusic
"""
from __future__ import annotations
@@ -28,7 +28,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserRecommendRateMusic(
def GetUserRecommendRateMusic(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -46,7 +46,7 @@ def UserRecommendRateMusic(
client_id=client_id, timeout_ms=timeout_ms, client=client)
def UserRecommendSelectMusic(
def GetUserRecommendSelectMusic(
user_id: int,
*,
client_id: str = CLIENT_ID,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_region.py —— 取用户地区游玩统计(GetUserRegionApi)
get_user_region.py —— 取用户地区游玩统计(GetUserRegionApi)
================================================================================
取玩家在各地区的游玩次数。
@@ -12,7 +12,7 @@ user_region.py —— 取用户地区游玩统计(GetUserRegionApi)
source/2026-10-05.log:77
调用示例:
from api import UserRegion
from api import GetUserRegion
"""
from __future__ import annotations
@@ -26,7 +26,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserIdRequestVO
def UserRegion(
def GetUserRegion(
user_id: int,
*,
client_id: str = CLIENT_ID,
+7 -7
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_rival.py —— 取用户对手数据(GetUserRivalDataApi / GetUserRivalMusicApi)
get_user_rival.py —— 取用户对手数据(GetUserRivalDataApi / GetUserRivalMusicApi)
================================================================================
取玩家的对手名单及其曲目成绩。
@@ -19,7 +19,7 @@ user_rival.py —— 取用户对手数据(GetUserRivalDataApi / GetUserRivalM
★ 对手 ID 需要调用方提供(先由 GetUserFavoriteItemApi 的 kind=2 取到)。
调用示例:
from api import UserRivalData, UserRivalMusic
from api import GetUserRivalData, GetUserRivalMusic
"""
from __future__ import annotations
@@ -35,11 +35,11 @@ from .user_getter_common import (
UserRivalRequestVO,
UserRivalMusicRequestVO,
RIVAL_MUSIC_LEVELS,
fetch_all_pages,
get_all_pages,
)
def UserRivalData(
def GetUserRivalData(
user_id: int,
rival_id: int,
*,
@@ -65,7 +65,7 @@ def UserRivalData(
)
def UserRivalMusic(
def GetUserRivalMusic(
user_id: int,
rival_id: int,
*,
@@ -100,7 +100,7 @@ def UserRivalMusic(
)
def UserRivalMusicAll(
def GetUserRivalMusicAll(
user_id: int,
rival_id: int,
*,
@@ -117,7 +117,7 @@ def UserRivalMusicAll(
"""
lv = list(levels) if levels else list(RIVAL_MUSIC_LEVELS)
log_info("取对手曲目成绩(全量):user_id=" + str(user_id) + " rival_id=" + str(rival_id))
items = fetch_all_pages(
items = get_all_pages(
"GetUserRivalMusicApi", user_id, "userRivalMusicList",
body_factory=lambda ni: UserRivalMusicRequestVO(
userId=user_id, rivalId=rival_id, nextIndex=ni or 0,
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
user_shop_stock.py —— 取用户商店库存(GetUserShopStockApi)
get_user_shop_stock.py —— 取用户商店库存(GetUserShopStockApi)
================================================================================
取玩家在各商店商品的已兑换次数。
@@ -15,7 +15,7 @@ user_shop_stock.py —— 取用户商店库存(GetUserShopStockApi)
不传时按空列表发送,服务端返回空 userShopStockList —— 这是合法的。
调用示例:
from api import UserShopStock
from api import GetUserShopStock
"""
from __future__ import annotations
@@ -29,7 +29,7 @@ from .maimai_net import MaimaiNetClient
from .user_getter_common import _get, UserShopStockRequestVO
def UserShopStock(
def GetUserShopStock(
user_id: int,
*,
shop_item_id_list: Optional[List[int]] = None,
+1 -1
View File
@@ -57,7 +57,7 @@ log_setup.py —— 统一日志模块(INFO / DEBUG 双模式)
from api import log_info, log_debug, log_request, log_response
def UserPreview(user_id, token, ...):
def GetUserPreview(user_id, token, ...):
# 函数的第一行 docstring 会成为日志里的「[接口说明]」
# 例如: '''获取用户预览信息'''
log_info(f"请求用户预览 user_id={user_id}") # 可选,想额外记话时才写
+30 -30
View File
@@ -85,16 +85,16 @@ source/2026-10-05.log:123 逐节点核对:
--------------------------------------------------------------------------------
五、调用示例
--------------------------------------------------------------------------------
from api import (setup_logging, ParseQRCode, UserPreview, UserLogin,
fetch_upsert_snapshot, UpsertUserAll, build_play_from_config)
from api import (setup_logging, ParseQRCode, GetUserPreview, UserLogin,
GetNeedUserData, UpsertUserAll, build_play_from_config)
setup_logging("结算")
user_id, token = ParseQRCode(qr)
UserPreview(user_id, token)
GetUserPreview(user_id, token)
login = UserLogin(user_id, token)
# 1) 登录后立刻抓基线快照(差分的基准)
snapshot = fetch_upsert_snapshot(user_id, token=token)
snapshot = GetNeedUserData(user_id, token=token)
# 2) 组装本局数据(默认读 config.py 的 PLAY_*)
play = build_play_from_config()
@@ -527,7 +527,7 @@ def build_favorite_delta(
"2" = 服务器侧有对应 itemKind(无论内容是否一致)
"1" = 服务器侧查不到该 itemKind
★ 关键:**优先用 Get 侧最原始的响应建索引**,不要先用 user_favorite.py
★ 关键:**优先用 Get 侧最原始的响应建索引**,不要先用 get_user_favorite.py
的「补 itemKind」逻辑归一化 —— 真机里 itemKind=10 的响应回的是
{"itemKind":null, "itemIdList":[]},正因为查不到才自然产生 "1"。
@@ -729,12 +729,12 @@ def derive_play_characters(
数据来源(都在 Get 响应里):
userData.charaSlot 本局 5 个角色槽的 characterId(顺序即 slot1..5)
source/Assembly-CSharp/Net.VO.Mai2/UserData.cs
source/Assembly-CSharp/Net.VO.Mai2/GetUserData.cs
userCharacterList 每只角色的 level / awakening
GetUserCharacterApi 响应
参数:
snapshot: fetch_upsert_snapshot 的返回值(或任何含 userData /
snapshot: GetNeedUserData 的返回值(或任何含 userData /
userCharacterList 的字典)。
play: 当前的 PlayData;快照里查不到的槽位回退到它同位置的配置。
@@ -811,7 +811,7 @@ def build_post_play_user_data(
**完全一致**(playCount=1258、point=4460、lastPlayDate=19:22:53.0)。
返回:
游玩后的 userData 字典(可直接放进 [ ] 传给 UserNewItemList)
游玩后的 userData 字典(可直接放进 [ ] 传给 GetUserNewItemList)
"""
p = play or build_play_from_config()
ua = build_user_all(
@@ -1265,7 +1265,7 @@ def _extract_from_get_responses(raw: Dict[str, Any]) -> Dict[str, Any]:
"userMapList": <GetUserMapApi 的响应>,
...
}
也接受 fetch_upsert_snapshot() 已经整理好的节点字典(幂等)。
也接受 GetNeedUserData() 已经整理好的节点字典(幂等)。
返回:
节点名 -> 节点值(已裁剪)
@@ -1318,7 +1318,7 @@ def _extract_from_get_responses(raw: Dict[str, Any]) -> Dict[str, Any]:
# 真机值 = 服务器下发的周界,**不是本地算的**:
# GetGameWeeklyDataApi.updateDate = "2026-09-28 04:00:00"
# Upsert 侧 lastLoginWeek = "2026-09-28 04:00:00" ← 完全一致
# 依据 source/Assembly-CSharp/Manager/UserData.cs:1040-1049
# 依据 source/Assembly-CSharp/Manager/GetUserData.cs:1040-1049
# WeeklyData.LastLoginWeek = gameWeeklyData.updateDate;
# 所以优先用快照里的值;只有拿不到时才退回本地推导。
if "userWeeklyData" not in out and "userMissionDataList" in raw:
@@ -1364,7 +1364,7 @@ def _extract_from_get_responses(raw: Dict[str, Any]) -> Dict[str, Any]:
# ★ 曲目成绩有两种形态,都要支持:
# a) 接口原始响应:两层嵌套(UserMusic.cs 外层按曲分组,
# 内层 userMusicDetailList 才是 (musicId, level) 条目)
# b) UserMusicDetailAll() 的产物:已摊平成单条
# b) GetUserMusicDetailAll() 的产物:已摊平成单条
# 判据:元素里带 "userMusicDetailList" 键 -> 嵌套;否则已摊平。
groups = src.get("userMusicList") or []
nested = any(
@@ -1382,7 +1382,7 @@ def _extract_from_get_responses(raw: Dict[str, Any]) -> Dict[str, Any]:
else:
items = src.get(list_key)
# ★ 兜底:取数脚本返回的字典有时用**节点名**作键
# (如 UserFavoriteAll -> {"userFavoriteList": [...], "_raw": [...]},
# (如 GetUserFavoriteAll -> {"userFavoriteList": [...], "_raw": [...]},
# 而 GetUserFavoriteApi 的原始响应是 {"userFavorite": {...}})。
if items is None:
items = src.get(node)
@@ -2152,7 +2152,7 @@ def _resolve_api_func(name: str, fallback: Any = None) -> Any:
★ 为什么要绕这一下:
autowrap() 只替换 api/__init__.py 命名空间里的名字
(见 log_setup.autowrap 的注释),模块内部的直接引用拿到的还是原始函数。
UpsertUserAll 内部要调 fetch_upsert_snapshot / UserNewItemList / UserData,
UpsertUserAll 内部要调 GetNeedUserData / GetUserNewItemList / GetUserData,
绕这一下它们才会像用户直接调用那样打印「=== xxx 开始运行 ===」,
主脚本就完全不用自己写日志了。
"""
@@ -2168,18 +2168,18 @@ def _resolve_api_func(name: str, fallback: Any = None) -> Any:
def _lazy(name: str) -> Any:
"""按名字延迟取 api 子模块里的原始函数(_resolve_api_func 的兜底)。"""
if name == "fetch_upsert_snapshot":
from .upsert_snapshot import fetch_upsert_snapshot
return fetch_upsert_snapshot
if name == "UserNewItemList":
from .user_new_item import UserNewItemList
return UserNewItemList
if name == "UserData":
from .userdata import UserData
return UserData
if name == "UserMusicDetailAll":
from .user_music import UserMusicDetailAll
return UserMusicDetailAll
if name == "GetNeedUserData":
from .get_need_user_data import GetNeedUserData
return GetNeedUserData
if name == "GetUserNewItemList":
from .get_user_new_item import GetUserNewItemList
return GetUserNewItemList
if name == "GetUserData":
from .get_user_data import GetUserData
return GetUserData
if name == "GetUserMusicDetailAll":
from .get_user_music import GetUserMusicDetailAll
return GetUserMusicDetailAll
raise PatchError("未知的内部 api:" + str(name))
@@ -2227,7 +2227,7 @@ def verify_upsert_landing(
before_ud = (base.get("userData") or [{}])[0] if base.get("userData") else {}
try:
after_raw = _api("UserData")(user_id, client_id=client_id,
after_raw = _api("GetUserData")(user_id, client_id=client_id,
timeout_ms=timeout_ms, client=client)
after_ud = (after_raw.get("userData") or {}) if isinstance(after_raw, dict) else {}
except Exception as exc:
@@ -2245,7 +2245,7 @@ def verify_upsert_landing(
# 本局曲目的 playCount
try:
music_raw = _api("UserMusicDetailAll")(user_id, client_id=client_id,
music_raw = _api("GetUserMusicDetailAll")(user_id, client_id=client_id,
timeout_ms=timeout_ms, client=client)
rows = music_raw.get("userMusicList") or [] if isinstance(music_raw, dict) else []
hit = [m for m in rows
@@ -2336,7 +2336,7 @@ def UpsertUserAll(
auth_date_time: UserLoginApi 请求里的 dateTime(可选;
userData.dateTime 要用它,真机实测两处一致)
token: 会话令牌(可选)。★ 只在「没传 snapshot 且 auto_snapshot=True」
时需要 —— 内部用它自动调 fetch_upsert_snapshot。
时需要 —— 内部用它自动调 GetNeedUserData。
snapshot: 基线快照(可选,见 normalize_snapshot)。
传了就用它;不传则按 auto_snapshot 自动抓。
★ 服务器对 userData 是整表覆盖、对差分节点是「只回传变化项」,
@@ -2390,7 +2390,7 @@ def UpsertUserAll(
"推荐传 token=...(自动抓)或 snapshot=...(自己抓好传进来)")
else:
log_info("未传 snapshot,自动抓取结算基线快照 ...")
snapshot = _api("fetch_upsert_snapshot")(
snapshot = _api("GetNeedUserData")(
user_id, token=token, client_id=client_id,
place_id=place_id, region_id=region_id, client=client,
)
@@ -2426,7 +2426,7 @@ def UpsertUserAll(
region_id=region_id, region_name=region_name, client_id=client_id,
play_date_time=play_now,
)
_api("UserNewItemList")(
_api("GetUserNewItemList")(
user_id, version=p.version_int,
user_data=[post_user_data],
user_playlog_list=list(user_playlog_list),
+5 -5
View File
@@ -10,7 +10,7 @@ upsert_user_chargelog.py —— 购买功能票 / 上传购买记录(UpsertUse
source/Assembly-CSharp/Net.VO.Mai2/UserChargelogRequestVO.cs
public ulong userId;
public UserChargelog userChargelog;
public UserCharge userCharge;
public GetUserCharge userCharge;
public long loginDateTime;
响应 :{returnCode, apiName}
@@ -26,7 +26,7 @@ upsert_user_chargelog.py —— 购买功能票 / 上传购买记录(UpsertUse
│ └─ 没有 -> 新增一条 stock=1
└─ TicketManager.cs:234 RequestUserCharge(playerId)
├─ VOExtensions.cs:436 ExportEnableUserCharge(ticketId)
│ -> 取**购买后**(stock > 0)的那条 UserCharge
│ -> 取**购买后**(stock > 0)的那条 GetUserCharge
├─ VOExtensions.cs:403 ExportUserChargelog(ticketId)
│ -> chargeId = ticketId
│ price = DataManager.GetTicket(ticketId).creditNum ★ 不是 money price
@@ -130,7 +130,7 @@ class ChargelogGuardError(RuntimeError):
@dataclass
class UserChargeVO:
"""UserCharge —— 一张票的持有状态(UserCharge.cs,4 字段)。"""
"""GetUserCharge —— 一张票的持有状态(GetUserCharge.cs,4 字段)。"""
chargeId: int = 0
stock: int = 0
purchaseDate: str = ""
@@ -349,13 +349,13 @@ def UpsertUserChargelog(
# ---------- 1. 取当前状态 ----------
if charge_list is None:
log_info("未传 charge_list,自动抓取当前票券 ...")
raw = resolve_api_func("UserCharge")(
raw = resolve_api_func("GetUserCharge")(
user_id, client_id=client_id, timeout_ms=timeout_ms, client=client)
charge_list = (raw.get("userChargeList") if isinstance(raw, dict) else None) or []
if user_data is None:
log_info("未传 user_data,自动抓取当前档案 ...")
raw_ud = resolve_api_func("UserData")(
raw_ud = resolve_api_func("GetUserData")(
user_id, client_id=client_id, timeout_ms=timeout_ms, client=client)
user_data = (raw_ud.get("userData") if isinstance(raw_ud, dict) else None) or {}
+2 -2
View File
@@ -7,7 +7,7 @@ user_getter_common.py —— 各取数脚本的公共底座
1. 各接口的请求 VO(严格对应 source/.../Net.VO.Mai2/*RequestVO.cs)
2. _get() 发一次请求并返回**原始响应 dict**
3. fetch_all_pages() 自动翻页
3. get_all_pages() 自动翻页
--------------------------------------------------------------------------------
★ 为什么单独抽出来:各接口的请求体并不统一
@@ -187,7 +187,7 @@ def _get(
return raw or {}
def fetch_all_pages(
def get_all_pages(
api_name: str,
user_id: int,
list_key: str,