重构: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:
@@ -1,4 +1,5 @@
|
|||||||
# ---- 运行产物 ----
|
# ---- 运行产物 ----
|
||||||
|
data/ # 每账号一份用户数据快照(<userId>.json)
|
||||||
logs/
|
logs/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|||||||
@@ -25,20 +25,26 @@ WaHappy 是一个用 **Python 纯网络方式**与舞萌DX(SDGB 国服)游
|
|||||||
|
|
||||||
**v0 目前只是一个占位版本,请以「能不能用」而不是「有多少代码」来判断它。**
|
**v0 目前只是一个占位版本,请以「能不能用」而不是「有多少代码」来判断它。**
|
||||||
|
|
||||||
| 模块 | 状态 |
|
| 脚本 | 状态 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **`upsert.py`** | ✅ **可用** —— 扫码登录 → 上传一局成绩 → 落库回查 → 登出 |
|
| **`upsert.py`** | ✅ **可用** —— 扫码登录 → 上传一局成绩 → 落库回查 → 登出 |
|
||||||
| `buy_ticket.py` | ⚠️ **不可用** —— 购买功能票接口疑似已被服务端拉黑,见下方警告 |
|
| **`buy_ticket.py`** | ✅ **可用** —— 购买功能票(2026-10-06 真机验证通过) |
|
||||||
|
| **`use_ticket.py`** | ✅ **可用** —— 读取当前持有的功能票并用 `UpsertUserAll` 消耗掉 |
|
||||||
|
| **`reincarnate.py`** | 🔧 已实现(未实跑)—— 账号数据迁移(转生),每包 ≤4 首 + 续关登录 |
|
||||||
| `api/` 下 40+ 个接口模块 | 🔧 已实现并通过零网络自检,但**未全部在真机上验证过** |
|
| `api/` 下 40+ 个接口模块 | 🔧 已实现并通过零网络自检,但**未全部在真机上验证过** |
|
||||||
| **文档** | 📄 在 **`docs` 分支**(`mai` 分支不含)—— 开发过程、踩坑记录、实跑验证,内容比代码本身还多 |
|
| **文档** | 📄 在 **`docs` 分支**(`mai` 分支不含)—— 开发过程、踩坑记录、实跑验证,内容比代码本身还多 |
|
||||||
|
|
||||||
### ⚠️ 关于购买功能票
|
### 关于购买功能票(已解决)
|
||||||
|
|
||||||
`buy_ticket.py`(`UpsertUserChargelogApi`)在**报文与真机逐字节一致**的情况下
|
`UpsertUserChargelogApi` 曾经一直返回 `returnCode=0`。根因是 **请求头多发了
|
||||||
仍被服务端拒绝(`returnCode=0`),之后该账号**疑似被拉黑,暂时无法再购买功能票**。
|
`Accept` 与 `Connection`** —— 真机(Unity Mono 的 `HttpWebRequest`)**不发这两个头**:
|
||||||
|
|
||||||
**请勿轻易重试该接口。** 完整排查过程见
|
```
|
||||||
[购买功能票流程](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/购买功能票流程.md) 第六节。
|
number / Content-Type / User-Agent / charset / Mai-Encoding / Content-Encoding / Cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
删掉之后一次通过(`stock` 0 → 1,落库确认)。完整排查过程见
|
||||||
|
[购买功能票流程](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs/docs/购买功能票流程.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -66,19 +72,23 @@ v0 的开发过程中,我反复把同一个目标拆成小段丢给 AI,结
|
|||||||
|
|
||||||
```
|
```
|
||||||
WaHappy/
|
WaHappy/
|
||||||
├── config.py 运行时可配置项(密钥 / 机台 / 歌曲数据 / 日志级别)
|
├── config.py 运行时可配置项(密钥 / 机台 / 歌曲数据 / 日志级别)★ 需自建
|
||||||
|
├── config.py.example 配置模板(机台与店铺标识留空待填)
|
||||||
├── upsert.py ★ 主脚本:扫码登录 → 上传成绩 → 登出
|
├── upsert.py ★ 主脚本:扫码登录 → 上传成绩 → 登出
|
||||||
├── buy_ticket.py 主脚本:购买功能票(★ 当前不可用)
|
├── buy_ticket.py 主脚本:购买功能票
|
||||||
├── selftest.py 零网络自检(236 项,判据 = 源码 + 真机日志)
|
├── use_ticket.py 主脚本:消耗当前持有的功能票
|
||||||
|
├── reincarnate.py 主脚本:账号数据迁移(转生)
|
||||||
|
├── selftest.py 零网络自检(238 项,判据 = 源码 + 真机日志)
|
||||||
│
|
│
|
||||||
├── api/ ★ 所有与服务器交互的模块(一个接口一个脚本)
|
├── api/ ★ 所有与服务器交互的模块(一个接口一个脚本)
|
||||||
│ ├── __init__.py 统一导出 + 日志自动跟踪
|
│ ├── __init__.py 统一导出 + 日志自动跟踪
|
||||||
│ ├── maimai_net.py 网络核心(AES / deflate / URL 混淆 / Cookie)
|
│ ├── maimai_net.py 网络核心(AES / deflate / URL 混淆 / Cookie)
|
||||||
│ ├── log_setup.py 日志(INFO / DEBUG 双模式)
|
│ ├── log_setup.py 日志(INFO / DEBUG 双模式)
|
||||||
│ ├── upsert_user_all.py 上传成绩(36 节点 / 110 字段 / 差分)
|
│ ├── upsert_user_all.py 上传成绩(36 节点 / 110 字段 / 差分)
|
||||||
│ ├── upsert_snapshot.py 结算基线快照
|
│ ├── get_need_user_data.py 结算快照编排 + 落盘 data/<userId>.json
|
||||||
│ └── ... 40+ 个接口脚本
|
│ └── ... 40+ 个接口脚本
|
||||||
│
|
│
|
||||||
|
├── data/ 每账号一份用户数据快照(<userId>.json)★ 不入库
|
||||||
├── logs/ 日志输出(latest.log + 日期.log)★ 不入库
|
├── logs/ 日志输出(latest.log + 日期.log)★ 不入库
|
||||||
└── source/ 反编译源码 ★ 不入库,需自备
|
└── source/ 反编译源码 ★ 不入库,需自备
|
||||||
```
|
```
|
||||||
@@ -86,6 +96,9 @@ WaHappy/
|
|||||||
**设计约定**:`api/` 里每个脚本 = 一个接口,对外暴露一个**能传参的函数**。
|
**设计约定**:`api/` 里每个脚本 = 一个接口,对外暴露一个**能传参的函数**。
|
||||||
主脚本只负责编排,日志和协议细节都在 `api/` 内部。
|
主脚本只负责编排,日志和协议细节都在 `api/` 内部。
|
||||||
|
|
||||||
|
**命名约定**:服务端 API 名里带 `Get` 的,**脚本名与函数名一律带 `get` / `Get`**
|
||||||
|
(不要模棱两可)。所以取数脚本全是 `get_*.py`,函数是 `GetXxx`。
|
||||||
|
|
||||||
> **`docs/` 在 [`docs` 分支](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs),`source/` 与 `logs/` 不入库。**
|
> **`docs/` 在 [`docs` 分支](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs),`source/` 与 `logs/` 不入库。**
|
||||||
>
|
>
|
||||||
> `source/` 是反编译源码,**需要自备**。
|
> `source/` 是反编译源码,**需要自备**。
|
||||||
@@ -158,6 +171,63 @@ python upsert.py "<二维码>" --send --no-verify # 跳过上传后的落库
|
|||||||
|
|
||||||
> 二维码从机台的「扫码登录」界面获取,**有效期 10 分钟**。
|
> 二维码从机台的「扫码登录」界面获取,**有效期 10 分钟**。
|
||||||
|
|
||||||
|
### buy_ticket.py 用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python buy_ticket.py "<二维码>" --list # 只列当前票券和可买的票
|
||||||
|
python buy_ticket.py "<二维码>" --charge-id 2 # 预检:只看报文
|
||||||
|
python buy_ticket.py "<二维码>" --charge-id 2 --send # ★ 真实购买
|
||||||
|
```
|
||||||
|
|
||||||
|
| chargeId | 票 | 上报的 price |
|
||||||
|
|---|---|---|
|
||||||
|
| 2 | 2倍チケット | 1 |
|
||||||
|
| 3 | 3倍チケット | 2 |
|
||||||
|
| 4 | 4倍チケット | 3 |
|
||||||
|
| 5 | 5倍チケット | 4 |
|
||||||
|
| 6 | 6倍チケット | 5 |
|
||||||
|
|
||||||
|
> ⚠️ **账号已经持有功能票时,本脚本会直接拒绝购买**(重复购买有封号风险)。
|
||||||
|
> 想清掉手里的票,用下面的 `use_ticket.py`。
|
||||||
|
|
||||||
|
### use_ticket.py 用法
|
||||||
|
|
||||||
|
读取账号当前**持有**的功能票,然后用 `UpsertUserAll` 表达「用这张票打了一局」把它消耗掉。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python use_ticket.py "<二维码>" --list # 只列当前持有的票
|
||||||
|
python use_ticket.py "<二维码>" --ticket-id 2 # 预检:只看报文
|
||||||
|
python use_ticket.py "<二维码>" --ticket-id 2 --send # ★ 真实消耗
|
||||||
|
```
|
||||||
|
|
||||||
|
原理:`GetUserChargeApi` 与 `UpsertUserAllApi` 里的 `userChargeList`
|
||||||
|
**格式完全相同**,所以只要把该节点里目标票的 `stock` 减 1,
|
||||||
|
再让 playlog 带上 `useTicketId` / `playCredit`,服务端就认为这张票被这一局用掉了。
|
||||||
|
|
||||||
|
`playCredit` 的算法(`VOExtensions.cs:838-845`):
|
||||||
|
|
||||||
|
```
|
||||||
|
playCredit = 投币数(普通局 = 1) + 票的 creditNum
|
||||||
|
```
|
||||||
|
|
||||||
|
### reincarnate.py 用法(账号数据迁移)
|
||||||
|
|
||||||
|
把一个账号的数据克隆到另一个账号。**每包最多 4 首**(真机一局最多 4 首),
|
||||||
|
每包之间用**续关登录**(`UserLogin(is_continue=True)`)换新的 `loginId`。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 源数据来自二维码(会联网抓 A 的数据并落盘到 data/<A>.json)
|
||||||
|
python reincarnate.py --dst-qr "<B的二维码>" --src-qr "<A的二维码>"
|
||||||
|
|
||||||
|
# 源数据来自已保存的 JSON(离线,推荐)
|
||||||
|
python reincarnate.py --dst-qr "<B的二维码>" --src-json data/12345678.json --plan
|
||||||
|
python reincarnate.py --dst-qr "<B的二维码>" --src-json data/12345678.json --send
|
||||||
|
|
||||||
|
# 试跑
|
||||||
|
--limit 20 # 只迁前 20 首
|
||||||
|
--clone-identity # 连 userName / accessCode 一起克隆(默认保留 B 自己的)
|
||||||
|
```
|
||||||
|
|
||||||
### 额外塞节点:`--extra-data`
|
### 额外塞节点:`--extra-data`
|
||||||
|
|
||||||
真机报文的 36 个节点绝大多数能由快照 + 本局演算自动推出来,
|
真机报文的 36 个节点绝大多数能由快照 + 本局演算自动推出来,
|
||||||
|
|||||||
+151
-134
@@ -26,47 +26,55 @@ api 包 —— 统一导出
|
|||||||
|
|
||||||
会话链路
|
会话链路
|
||||||
chime.py 扫码登录 -> userId + token
|
chime.py 扫码登录 -> userId + token
|
||||||
gamesettings.py 取游戏设置
|
get_game_settings.py GetGameSettingApi(游戏设置)
|
||||||
ping.py 心跳探活
|
ping.py 心跳探活
|
||||||
userpreview.py 用户预览
|
get_user_preview.py GetUserPreviewApi(用户预览)
|
||||||
userlogin.py 正式登录 -> loginId + loginDateTime
|
userlogin.py UserLoginApi(正式登录 -> loginId + loginDateTime)
|
||||||
userlogout.py 登出
|
userlogout.py UserLogoutApi(登出)
|
||||||
|
|
||||||
上传成绩
|
上传 / 结算(有副作用的接口)
|
||||||
upsert_user_all.py ★ 上传成绩(唯一有副作用的接口)
|
upsert_user_all.py ★ UpsertUserAllApi(上传成绩)
|
||||||
upsert_snapshot.py 结算快照编排(统一调用下面的取数脚本)
|
upsert_user_chargelog.py UpsertUserChargelogApi(购买功能票)
|
||||||
|
get_need_user_data.py ★ 结算快照编排(统一调用下面的取数脚本),
|
||||||
|
并把结果落盘到 <工程根>/data/<userId>.json
|
||||||
|
|
||||||
取数(一个接口一个脚本)
|
★ 命名约定:服务端 API 名里带 Get 的,**脚本名与函数名一律带 get / Get**
|
||||||
userdata.py GetUserDataApi
|
(不要模棱两可)。所以下面这些文件全是 get_ 开头。
|
||||||
user_extend.py GetUserExtendApi
|
|
||||||
user_option.py GetUserOptionApi
|
取数 —— GetUser*(一个接口一个脚本)
|
||||||
user_character.py GetUserCharacterApi
|
get_user_data.py GetUserDataApi
|
||||||
user_ghost.py GetUserGhostApi
|
get_user_extend.py GetUserExtendApi
|
||||||
user_map.py GetUserMapApi
|
get_user_option.py GetUserOptionApi
|
||||||
user_login_bonus.py GetUserLoginBonusApi
|
get_user_character.py GetUserCharacterApi
|
||||||
user_rating.py GetUserRatingApi
|
get_user_ghost.py GetUserGhostApi
|
||||||
user_item.py GetUserItemApi
|
get_user_map.py GetUserMapApi
|
||||||
user_music.py GetUserMusicApi
|
get_user_login_bonus.py GetUserLoginBonusApi
|
||||||
user_course.py GetUserCourseApi
|
get_user_rating.py GetUserRatingApi
|
||||||
user_charge.py GetUserChargeApi
|
get_user_item.py GetUserItemApi
|
||||||
user_favorite.py GetUserFavoriteApi
|
get_user_music.py GetUserMusicApi
|
||||||
user_activity.py GetUserActivityApi
|
get_user_course.py GetUserCourseApi
|
||||||
user_mission.py GetUserMissionDataApi
|
get_user_charge.py GetUserChargeApi
|
||||||
user_intimate.py GetUserIntimateApi
|
get_user_favorite.py GetUserFavoriteApi
|
||||||
user_shop_stock.py GetUserShopStockApi
|
get_user_activity.py GetUserActivityApi
|
||||||
user_favorite_item.py GetUserFavoriteItemApi
|
get_user_mission.py GetUserMissionDataApi
|
||||||
user_rival.py GetUserRivalDataApi / GetUserRivalMusicApi
|
get_user_intimate.py GetUserIntimateApi
|
||||||
user_friend_bonus.py GetUserFriendBonusApi
|
get_user_shop_stock.py GetUserShopStockApi
|
||||||
user_kaleidx_scope.py GetUserKaleidxScopeApi / GetGameKaleidxScopeApi
|
get_user_favorite_item.py GetUserFavoriteItemApi
|
||||||
user_region.py GetUserRegionApi
|
get_user_rival.py GetUserRivalDataApi / GetUserRivalMusicApi
|
||||||
user_recommend_music.py GetUserRecommendRateMusicApi / GetUserRecommendSelectMusicApi
|
get_user_friend_bonus.py GetUserFriendBonusApi
|
||||||
user_new_item.py GetUserNewItemListApi
|
get_user_kaleidx_scope.py GetUserKaleidxScopeApi / GetGameKaleidxScopeApi
|
||||||
game_ranking.py GetGameRankingApi
|
get_user_region.py GetUserRegionApi
|
||||||
game_event.py GetGameEventApi
|
get_user_recommend_music.py GetUserRecommendRateMusicApi / GetUserRecommendSelectMusicApi
|
||||||
game_charge.py GetGameChargeApi
|
get_user_new_item.py GetUserNewItemListApi
|
||||||
game_weekly_data.py GetGameWeeklyDataApi
|
|
||||||
game_tournament_info.py GetGameTournamentInfoApi
|
取数 —— GetGame*
|
||||||
game_ng_music_id.py GetGameNgMusicIdApi
|
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 (
|
from .log_setup import (
|
||||||
@@ -135,7 +143,7 @@ from .maimai_net import (
|
|||||||
|
|
||||||
from .chime import ParseQRCode
|
from .chime import ParseQRCode
|
||||||
|
|
||||||
from .gamesettings import (
|
from .get_game_settings import (
|
||||||
GetGameSettings,
|
GetGameSettings,
|
||||||
GameSetting,
|
GameSetting,
|
||||||
GameSettingRequestVO,
|
GameSettingRequestVO,
|
||||||
@@ -146,8 +154,8 @@ from .gamesettings import (
|
|||||||
|
|
||||||
from .ping import Ping
|
from .ping import Ping
|
||||||
|
|
||||||
from .userpreview import (
|
from .get_user_preview import (
|
||||||
UserPreview,
|
GetUserPreview,
|
||||||
UserPreviewRequestVO,
|
UserPreviewRequestVO,
|
||||||
UserPreviewResponseVO,
|
UserPreviewResponseVO,
|
||||||
)
|
)
|
||||||
@@ -230,12 +238,19 @@ from .upsert_user_all import (
|
|||||||
USER_MISSION_DROP_KEYS,
|
USER_MISSION_DROP_KEYS,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .upsert_snapshot import (
|
from .get_need_user_data import (
|
||||||
fetch_upsert_snapshot,
|
GetNeedUserData,
|
||||||
save_snapshot,
|
save_snapshot,
|
||||||
load_snapshot,
|
load_snapshot,
|
||||||
summarize_snapshot,
|
summarize_snapshot,
|
||||||
DEFAULT_SNAPSHOT_NODES,
|
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 (
|
from .upsert_user_chargelog import (
|
||||||
@@ -269,7 +284,7 @@ from .user_getter_common import (
|
|||||||
UserShopStockRequestVO,
|
UserShopStockRequestVO,
|
||||||
UserRivalRequestVO,
|
UserRivalRequestVO,
|
||||||
UserRivalMusicRequestVO,
|
UserRivalMusicRequestVO,
|
||||||
fetch_all_pages,
|
get_all_pages,
|
||||||
ITEM_KINDS,
|
ITEM_KINDS,
|
||||||
ITEM_NEXT_INDEX_FACTOR,
|
ITEM_NEXT_INDEX_FACTOR,
|
||||||
FAVORITE_ITEM_KINDS,
|
FAVORITE_ITEM_KINDS,
|
||||||
@@ -280,42 +295,42 @@ from .user_getter_common import (
|
|||||||
# 5) 取数脚本(一个接口一个脚本)
|
# 5) 取数脚本(一个接口一个脚本)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
from .userdata import UserData
|
from .get_user_data import GetUserData
|
||||||
from .user_extend import UserExtend
|
from .get_user_extend import GetUserExtend
|
||||||
from .user_option import UserOption
|
from .get_user_option import GetUserOption
|
||||||
from .user_character import UserCharacter
|
from .get_user_character import GetUserCharacter
|
||||||
from .user_ghost import UserGhost
|
from .get_user_ghost import GetUserGhost
|
||||||
from .user_map import UserMap, UserMapAll
|
from .get_user_map import GetUserMap, GetUserMapAll
|
||||||
from .user_login_bonus import UserLoginBonus, UserLoginBonusAll
|
from .get_user_login_bonus import GetUserLoginBonus, GetUserLoginBonusAll
|
||||||
from .user_rating import UserRating
|
from .get_user_rating import GetUserRating
|
||||||
from .user_item import UserItem, UserItemAll
|
from .get_user_item import GetUserItem, GetUserItemAll
|
||||||
from .user_music import UserMusicDetail, UserMusicDetailAll
|
from .get_user_music import GetUserMusicDetail, GetUserMusicDetailAll
|
||||||
from .user_course import UserCourse, UserCourseAll
|
from .get_user_course import GetUserCourse, GetUserCourseAll
|
||||||
from .user_charge import UserCharge
|
from .get_user_charge import GetUserCharge
|
||||||
from .user_favorite import UserFavorite, UserFavoriteAll
|
from .get_user_favorite import GetUserFavorite, GetUserFavoriteAll
|
||||||
from .user_activity import UserActivity
|
from .get_user_activity import GetUserActivity
|
||||||
from .user_mission import UserMissionData
|
from .get_user_mission import GetUserMissionData
|
||||||
from .user_intimate import UserIntimate
|
from .get_user_intimate import GetUserIntimate
|
||||||
from .user_shop_stock import UserShopStock
|
from .get_user_shop_stock import GetUserShopStock
|
||||||
from .user_favorite_item import (
|
from .get_user_favorite_item import (
|
||||||
UserFavoriteItem,
|
GetUserFavoriteItem,
|
||||||
UserFavoriteItemAll,
|
GetUserFavoriteItemAll,
|
||||||
FAVORITE_ITEM_KIND_MUSIC,
|
FAVORITE_ITEM_KIND_MUSIC,
|
||||||
FAVORITE_ITEM_KIND_RIVAL,
|
FAVORITE_ITEM_KIND_RIVAL,
|
||||||
)
|
)
|
||||||
from .user_rival import UserRivalData, UserRivalMusic, UserRivalMusicAll
|
from .get_user_rival import GetUserRivalData, GetUserRivalMusic, GetUserRivalMusicAll
|
||||||
from .user_friend_bonus import UserFriendBonus, extract_gift_points
|
from .get_user_friend_bonus import GetUserFriendBonus, extract_gift_points
|
||||||
from .user_kaleidx_scope import UserKaleidxScope, GameKaleidxScope
|
from .get_user_kaleidx_scope import GetUserKaleidxScope, GetGameKaleidxScope
|
||||||
from .user_region import UserRegion
|
from .get_user_region import GetUserRegion
|
||||||
from .user_recommend_music import UserRecommendRateMusic, UserRecommendSelectMusic
|
from .get_user_recommend_music import GetUserRecommendRateMusic, GetUserRecommendSelectMusic
|
||||||
from .user_new_item import UserNewItemList, UserNewItemListRequestVO
|
from .get_user_new_item import GetUserNewItemList, UserNewItemListRequestVO
|
||||||
|
|
||||||
from .game_ranking import GetGameRanking, GameRankingRequestVO
|
from .get_game_ranking import GetGameRanking, GameRankingRequestVO
|
||||||
from .game_event import GetGameEvent, GameEventRequestVO
|
from .get_game_event import GetGameEvent, GameEventRequestVO
|
||||||
from .game_charge import GetGameCharge, GameChargeRequestVO
|
from .get_game_charge import GetGameCharge, GameChargeRequestVO
|
||||||
from .game_weekly_data import GetGameWeeklyData
|
from .get_game_weekly_data import GetGameWeeklyData
|
||||||
from .game_tournament_info import GetGameTournamentInfo
|
from .get_game_tournament_info import GetGameTournamentInfo
|
||||||
from .game_ng_music_id import GetGameNgMusicId
|
from .get_game_ng_music_id import GetGameNgMusicId
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 6) 需要日志跟踪的 api 函数(新增 api 时把函数名加到这里)
|
# 6) 需要日志跟踪的 api 函数(新增 api 时把函数名加到这里)
|
||||||
@@ -326,7 +341,7 @@ _API_FUNCS = (
|
|||||||
"ParseQRCode",
|
"ParseQRCode",
|
||||||
"GetGameSettings",
|
"GetGameSettings",
|
||||||
"Ping",
|
"Ping",
|
||||||
"UserPreview",
|
"GetUserPreview",
|
||||||
"UserLogin",
|
"UserLogin",
|
||||||
"UserLogout",
|
"UserLogout",
|
||||||
# --- 上传成绩 ---
|
# --- 上传成绩 ---
|
||||||
@@ -350,45 +365,45 @@ _API_FUNCS = (
|
|||||||
"build_delta",
|
"build_delta",
|
||||||
"build_favorite_delta",
|
"build_favorite_delta",
|
||||||
"normalize_snapshot",
|
"normalize_snapshot",
|
||||||
"fetch_upsert_snapshot",
|
"GetNeedUserData",
|
||||||
"summarize_snapshot",
|
"summarize_snapshot",
|
||||||
"UserNewItemList",
|
"GetUserNewItemList",
|
||||||
# --- 取数:单接口 ---
|
# --- 取数:单接口 ---
|
||||||
"UserData", # GetUserDataApi
|
"GetUserData", # GetUserDataApi
|
||||||
"UserExtend", # GetUserExtendApi
|
"GetUserExtend", # GetUserExtendApi
|
||||||
"UserOption", # GetUserOptionApi
|
"GetUserOption", # GetUserOptionApi
|
||||||
"UserCharacter", # GetUserCharacterApi
|
"GetUserCharacter", # GetUserCharacterApi
|
||||||
"UserGhost", # GetUserGhostApi
|
"GetUserGhost", # GetUserGhostApi
|
||||||
"UserMap", # GetUserMapApi(单页)
|
"GetUserMap", # GetUserMapApi(单页)
|
||||||
"UserMapAll", # GetUserMapApi(全量)
|
"GetUserMapAll", # GetUserMapApi(全量)
|
||||||
"UserLoginBonus", # GetUserLoginBonusApi(单页)
|
"GetUserLoginBonus", # GetUserLoginBonusApi(单页)
|
||||||
"UserLoginBonusAll", # GetUserLoginBonusApi(全量)
|
"GetUserLoginBonusAll", # GetUserLoginBonusApi(全量)
|
||||||
"UserRating", # GetUserRatingApi
|
"GetUserRating", # GetUserRatingApi
|
||||||
"UserItem", # GetUserItemApi(单类)
|
"GetUserItem", # GetUserItemApi(单类)
|
||||||
"UserItemAll", # GetUserItemApi(全量)
|
"GetUserItemAll", # GetUserItemApi(全量)
|
||||||
"UserMusicDetail", # GetUserMusicApi(单页)
|
"GetUserMusicDetail", # GetUserMusicApi(单页)
|
||||||
"UserMusicDetailAll", # GetUserMusicApi(全量)
|
"GetUserMusicDetailAll", # GetUserMusicApi(全量)
|
||||||
"UserCourse", # GetUserCourseApi(单页)
|
"GetUserCourse", # GetUserCourseApi(单页)
|
||||||
"UserCourseAll", # GetUserCourseApi(全量)
|
"GetUserCourseAll", # GetUserCourseApi(全量)
|
||||||
"UserCharge", # GetUserChargeApi
|
"GetUserCharge", # GetUserChargeApi
|
||||||
"UserFavorite", # GetUserFavoriteApi(单类)
|
"GetUserFavorite", # GetUserFavoriteApi(单类)
|
||||||
"UserFavoriteAll", # GetUserFavoriteApi(全量)
|
"GetUserFavoriteAll", # GetUserFavoriteApi(全量)
|
||||||
"UserActivity", # GetUserActivityApi
|
"GetUserActivity", # GetUserActivityApi
|
||||||
"UserMissionData", # GetUserMissionDataApi
|
"GetUserMissionData", # GetUserMissionDataApi
|
||||||
"UserIntimate", # GetUserIntimateApi
|
"GetUserIntimate", # GetUserIntimateApi
|
||||||
"UserShopStock", # GetUserShopStockApi
|
"GetUserShopStock", # GetUserShopStockApi
|
||||||
"UserFavoriteItem", # GetUserFavoriteItemApi(单页)
|
"GetUserFavoriteItem", # GetUserFavoriteItemApi(单页)
|
||||||
"UserFavoriteItemAll", # GetUserFavoriteItemApi(全量)
|
"GetUserFavoriteItemAll", # GetUserFavoriteItemApi(全量)
|
||||||
"UserRivalData", # GetUserRivalDataApi
|
"GetUserRivalData", # GetUserRivalDataApi
|
||||||
"UserRivalMusic", # GetUserRivalMusicApi(单页)
|
"GetUserRivalMusic", # GetUserRivalMusicApi(单页)
|
||||||
"UserRivalMusicAll", # GetUserRivalMusicApi(全量)
|
"GetUserRivalMusicAll", # GetUserRivalMusicApi(全量)
|
||||||
"UserFriendBonus", # GetUserFriendBonusApi
|
"GetUserFriendBonus", # GetUserFriendBonusApi
|
||||||
"UserKaleidxScope", # GetUserKaleidxScopeApi
|
"GetUserKaleidxScope", # GetUserKaleidxScopeApi
|
||||||
"GameKaleidxScope", # GetGameKaleidxScopeApi
|
"GetGameKaleidxScope", # GetGameKaleidxScopeApi
|
||||||
"UserRegion", # GetUserRegionApi
|
"GetUserRegion", # GetUserRegionApi
|
||||||
"UserRecommendRateMusic", # GetUserRecommendRateMusicApi
|
"GetUserRecommendRateMusic", # GetUserRecommendRateMusicApi
|
||||||
"UserRecommendSelectMusic", # GetUserRecommendSelectMusicApi
|
"GetUserRecommendSelectMusic", # GetUserRecommendSelectMusicApi
|
||||||
"UserNewItemList", # GetUserNewItemListApi
|
"GetUserNewItemList", # GetUserNewItemListApi
|
||||||
# --- 取数:游戏侧 ---
|
# --- 取数:游戏侧 ---
|
||||||
"GetGameRanking", # GetGameRankingApi
|
"GetGameRanking", # GetGameRankingApi
|
||||||
"GetGameEvent", # GetGameEventApi
|
"GetGameEvent", # GetGameEventApi
|
||||||
@@ -435,7 +450,7 @@ __all__ = [
|
|||||||
"GetGameSettings", "GameSetting", "GameSettingRequestVO", "GameSettingResponseVO",
|
"GetGameSettings", "GameSetting", "GameSettingRequestVO", "GameSettingResponseVO",
|
||||||
"is_maintenance", "is_ping_disabled",
|
"is_maintenance", "is_ping_disabled",
|
||||||
"Ping",
|
"Ping",
|
||||||
"UserPreview", "UserPreviewRequestVO", "UserPreviewResponseVO",
|
"GetUserPreview", "UserPreviewRequestVO", "UserPreviewResponseVO",
|
||||||
"UserLogin", "UserLoginRequestVO", "UserLoginResponseVO",
|
"UserLogin", "UserLoginRequestVO", "UserLoginResponseVO",
|
||||||
"UserLogout", "LogoutType", "DelayLog", "RequestAPI",
|
"UserLogout", "LogoutType", "DelayLog", "RequestAPI",
|
||||||
"UserLogoutRequestVO", "UserLogoutRequest2VO", "UserLogoutResponseVO",
|
"UserLogoutRequestVO", "UserLogoutRequest2VO", "UserLogoutResponseVO",
|
||||||
@@ -462,34 +477,36 @@ __all__ = [
|
|||||||
"USER_DATA_DROP_KEYS", "USER_OPTION_DROP_KEYS", "USER_CHARACTER_KEEP_KEYS",
|
"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",
|
"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",
|
"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",
|
"GetterEmptyRequestVO", "GetterUserIdRequestVO", "UserIdPageRequestVO",
|
||||||
"UserLoginBonusRequestVO", "UserItemRequestVO", "UserMusicRequestVO",
|
"UserLoginBonusRequestVO", "UserItemRequestVO", "UserMusicRequestVO",
|
||||||
"UserCourseRequestVO", "UserFavoriteRequestVO", "UserFavoriteItemRequestVO",
|
"UserCourseRequestVO", "UserFavoriteRequestVO", "UserFavoriteItemRequestVO",
|
||||||
"UserShopStockRequestVO", "UserRivalRequestVO", "UserRivalMusicRequestVO",
|
"UserShopStockRequestVO", "UserRivalRequestVO", "UserRivalMusicRequestVO",
|
||||||
"fetch_all_pages",
|
"get_all_pages",
|
||||||
"ITEM_KINDS", "ITEM_NEXT_INDEX_FACTOR", "FAVORITE_ITEM_KINDS", "RIVAL_MUSIC_LEVELS",
|
"ITEM_KINDS", "ITEM_NEXT_INDEX_FACTOR", "FAVORITE_ITEM_KINDS", "RIVAL_MUSIC_LEVELS",
|
||||||
# ---- 取数脚本 ----
|
# ---- 取数脚本 ----
|
||||||
"UserData", "UserExtend", "UserOption", "UserCharacter", "UserGhost",
|
"GetUserData", "GetUserExtend", "GetUserOption", "GetUserCharacter", "GetUserGhost",
|
||||||
"UserMap", "UserMapAll",
|
"GetUserMap", "GetUserMapAll",
|
||||||
"UserLoginBonus", "UserLoginBonusAll",
|
"GetUserLoginBonus", "GetUserLoginBonusAll",
|
||||||
"UserRating",
|
"GetUserRating",
|
||||||
"UserItem", "UserItemAll",
|
"GetUserItem", "GetUserItemAll",
|
||||||
"UserMusicDetail", "UserMusicDetailAll",
|
"GetUserMusicDetail", "GetUserMusicDetailAll",
|
||||||
"UserCourse", "UserCourseAll",
|
"GetUserCourse", "GetUserCourseAll",
|
||||||
"UserCharge",
|
"GetUserCharge",
|
||||||
"UserFavorite", "UserFavoriteAll",
|
"GetUserFavorite", "GetUserFavoriteAll",
|
||||||
"UserActivity", "UserMissionData", "UserIntimate", "UserShopStock",
|
"GetUserActivity", "GetUserMissionData", "GetUserIntimate", "GetUserShopStock",
|
||||||
"UserFavoriteItem", "UserFavoriteItemAll",
|
"GetUserFavoriteItem", "GetUserFavoriteItemAll",
|
||||||
"FAVORITE_ITEM_KIND_MUSIC", "FAVORITE_ITEM_KIND_RIVAL",
|
"FAVORITE_ITEM_KIND_MUSIC", "FAVORITE_ITEM_KIND_RIVAL",
|
||||||
"UserRivalData", "UserRivalMusic", "UserRivalMusicAll",
|
"GetUserRivalData", "GetUserRivalMusic", "GetUserRivalMusicAll",
|
||||||
"UserFriendBonus", "extract_gift_points",
|
"GetUserFriendBonus", "extract_gift_points",
|
||||||
"UserKaleidxScope", "GameKaleidxScope",
|
"GetUserKaleidxScope", "GetGameKaleidxScope",
|
||||||
"UserRegion",
|
"GetUserRegion",
|
||||||
"UserRecommendRateMusic", "UserRecommendSelectMusic",
|
"GetUserRecommendRateMusic", "GetUserRecommendSelectMusic",
|
||||||
"UserNewItemList", "UserNewItemListRequestVO",
|
"GetUserNewItemList", "UserNewItemListRequestVO",
|
||||||
"GetGameRanking", "GameRankingRequestVO",
|
"GetGameRanking", "GameRankingRequestVO",
|
||||||
"GetGameEvent", "GameEventRequestVO",
|
"GetGameEvent", "GameEventRequestVO",
|
||||||
"GetGameCharge", "GameChargeRequestVO",
|
"GetGameCharge", "GameChargeRequestVO",
|
||||||
|
|||||||
+1
-1
@@ -235,7 +235,7 @@ def ParseQRCode(qr_input: str, *, chip_id: str = DEFAULT_CHIP_ID) -> Tuple[int,
|
|||||||
chip_id: 机台 Keychip 短值(可选,默认取 config.CLIENT_ID)
|
chip_id: 机台 Keychip 短值(可选,默认取 config.CLIENT_ID)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
(user_id, token) 元组,可直接传给 UserPreview / UserLogin 等
|
(user_id, token) 元组,可直接传给 GetUserPreview / UserLogin 等
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
ValueError: 二维码格式不合法
|
ValueError: 二维码格式不合法
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
game_charge.py —— 取游戏计费配置(GetGameChargeApi)
|
get_game_charge.py —— 取游戏计费配置(GetGameChargeApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取服务器下发的计费档位。
|
取服务器下发的计费档位。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
game_event.py —— 取游戏活动(GetGameEventApi)
|
get_game_event.py —— 取游戏活动(GetGameEventApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取服务器下发的活动列表。
|
取服务器下发的活动列表。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
game_ng_music_id.py —— 取禁用曲目列表(GetGameNgMusicIdApi)
|
get_game_ng_music_id.py —— 取禁用曲目列表(GetGameNgMusicIdApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取服务器下发的 NG(不可游玩)曲目 ID 列表。
|
取服务器下发的 NG(不可游玩)曲目 ID 列表。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
game_ranking.py —— 取游戏排行(GetGameRankingApi)
|
get_game_ranking.py —— 取游戏排行(GetGameRankingApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取服务器下发的排行榜数据。
|
取服务器下发的排行榜数据。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
gamesettings.py —— 获取游戏设置(GetGameSettingApi)
|
get_game_settings.py —— 获取游戏设置(GetGameSettingApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
功能:取服务器下发的运行时配置(维护窗口、心跳间隔、包超时、各类上限)。
|
功能:取服务器下发的运行时配置(维护窗口、心跳间隔、包超时、各类上限)。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
game_tournament_info.py —— 取大会信息(GetGameTournamentInfoApi)
|
get_game_tournament_info.py —— 取大会信息(GetGameTournamentInfoApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取服务器下发的大会/赛事信息。
|
取服务器下发的大会/赛事信息。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
game_weekly_data.py —— 取游戏周常配置(GetGameWeeklyDataApi)
|
get_game_weekly_data.py —— 取游戏周常配置(GetGameWeeklyDataApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取服务器下发的周常周期信息。
|
取服务器下发的周常周期信息。
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
upsert_snapshot.py —— 结算快照编排(统一调用各取数脚本)
|
get_need_user_data.py —— 结算快照编排(统一调用各取数脚本)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
功能:一键抓取 UpsertUserAll 需要的全部基线数据,整理成「节点字典」。
|
功能:一键抓取 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, ...)
|
# 之后直接传给 UpsertUserAll(snapshot=snapshot, ...)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Sequence
|
from typing import Any, Dict, List, Optional, Sequence
|
||||||
|
|
||||||
from config import CLIENT_ID, PLACE_ID, PLAY_SHOP_ITEM_IDS, REGION_ID, TIMEOUT_MS
|
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 .maimai_net import MaimaiNetClient
|
||||||
|
|
||||||
# 各取数脚本(一个接口一个脚本)
|
# 各取数脚本(一个接口一个脚本)
|
||||||
from .userdata import UserData
|
from .get_user_data import GetUserData
|
||||||
from .user_extend import UserExtend
|
from .get_user_extend import GetUserExtend
|
||||||
from .user_option import UserOption
|
from .get_user_option import GetUserOption
|
||||||
from .user_character import UserCharacter
|
from .get_user_character import GetUserCharacter
|
||||||
from .user_ghost import UserGhost
|
from .get_user_ghost import GetUserGhost
|
||||||
from .user_map import UserMapAll
|
from .get_user_map import GetUserMapAll
|
||||||
from .user_login_bonus import UserLoginBonusAll
|
from .get_user_login_bonus import GetUserLoginBonusAll
|
||||||
from .user_rating import UserRating
|
from .get_user_rating import GetUserRating
|
||||||
from .user_item import UserItemAll
|
from .get_user_item import GetUserItemAll
|
||||||
from .user_music import UserMusicDetailAll
|
from .get_user_music import GetUserMusicDetailAll
|
||||||
from .user_course import UserCourseAll
|
from .get_user_course import GetUserCourseAll
|
||||||
from .user_charge import UserCharge
|
from .get_user_charge import GetUserCharge
|
||||||
from .user_favorite import UserFavoriteAll
|
from .get_user_favorite import GetUserFavoriteAll
|
||||||
from .user_activity import UserActivity
|
from .get_user_activity import GetUserActivity
|
||||||
from .user_mission import UserMissionData
|
from .get_user_mission import GetUserMissionData
|
||||||
from .user_intimate import UserIntimate
|
from .get_user_intimate import GetUserIntimate
|
||||||
from .user_shop_stock import UserShopStock
|
from .get_user_shop_stock import GetUserShopStock
|
||||||
from .user_favorite_item import UserFavoriteItemAll
|
from .get_user_favorite_item import GetUserFavoriteItemAll
|
||||||
from .user_kaleidx_scope import UserKaleidxScope
|
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,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
token: str = "",
|
token: str = "",
|
||||||
@@ -101,6 +181,8 @@ def fetch_upsert_snapshot(
|
|||||||
timeout_ms: Optional[int] = None,
|
timeout_ms: Optional[int] = None,
|
||||||
client: Optional[MaimaiNetClient] = None,
|
client: Optional[MaimaiNetClient] = None,
|
||||||
strict: bool = False,
|
strict: bool = False,
|
||||||
|
save: bool = True,
|
||||||
|
save_path: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
抓取结算所需的全部基线数据。
|
抓取结算所需的全部基线数据。
|
||||||
@@ -117,6 +199,9 @@ def fetch_upsert_snapshot(
|
|||||||
client: 可复用的 MaimaiNetClient
|
client: 可复用的 MaimaiNetClient
|
||||||
strict: True = 任一接口失败就抛异常;
|
strict: True = 任一接口失败就抛异常;
|
||||||
False(默认)= 记录失败并跳过,保证链路不崩
|
False(默认)= 记录失败并跳过,保证链路不崩
|
||||||
|
save: True(默认)= 抓完自动落盘到 <工程根>/data/<userId>.json
|
||||||
|
(★ 新数据总是覆盖老数据)
|
||||||
|
save_path: 自定义落盘路径(可选,给了就忽略 save 的默认路径规则)
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
{
|
{
|
||||||
@@ -151,78 +236,87 @@ def fetch_upsert_snapshot(
|
|||||||
|
|
||||||
# ---- 逐个抓取(顺序与 DEFAULT_SNAPSHOT_NODES 一致)----
|
# ---- 逐个抓取(顺序与 DEFAULT_SNAPSHOT_NODES 一致)----
|
||||||
if "userData" in wanted:
|
if "userData" in wanted:
|
||||||
_call("userData", UserData, user_id, **common)
|
_call("userData", GetUserData, user_id, **common)
|
||||||
|
|
||||||
if "userExtend" in wanted:
|
if "userExtend" in wanted:
|
||||||
_call("userExtend", UserExtend, user_id, **common)
|
_call("userExtend", GetUserExtend, user_id, **common)
|
||||||
|
|
||||||
if "userOption" in wanted:
|
if "userOption" in wanted:
|
||||||
_call("userOption", UserOption, user_id, **common)
|
_call("userOption", GetUserOption, user_id, **common)
|
||||||
|
|
||||||
if "userCharacterList" in wanted:
|
if "userCharacterList" in wanted:
|
||||||
_call("userCharacterList", UserCharacter, user_id, **common)
|
_call("userCharacterList", GetUserCharacter, user_id, **common)
|
||||||
|
|
||||||
if "userMapList" in wanted:
|
if "userMapList" in wanted:
|
||||||
_call("userMapList", UserMapAll, user_id, **common)
|
_call("userMapList", GetUserMapAll, user_id, **common)
|
||||||
|
|
||||||
if "userLoginBonusList" in wanted:
|
if "userLoginBonusList" in wanted:
|
||||||
_call("userLoginBonusList", UserLoginBonusAll, user_id, **common)
|
_call("userLoginBonusList", GetUserLoginBonusAll, user_id, **common)
|
||||||
|
|
||||||
if "userRatingList" in wanted:
|
if "userRatingList" in wanted:
|
||||||
_call("userRatingList", UserRating, user_id, **common)
|
_call("userRatingList", GetUserRating, user_id, **common)
|
||||||
|
|
||||||
if "userItemList" in wanted:
|
if "userItemList" in wanted:
|
||||||
_call("userItemList", UserItemAll, user_id, **common)
|
_call("userItemList", GetUserItemAll, user_id, **common)
|
||||||
|
|
||||||
if "userMusicDetailList" in wanted:
|
if "userMusicDetailList" in wanted:
|
||||||
_call("userMusicDetailList", UserMusicDetailAll, user_id, **common)
|
_call("userMusicDetailList", GetUserMusicDetailAll, user_id, **common)
|
||||||
|
|
||||||
if "userCourseList" in wanted:
|
if "userCourseList" in wanted:
|
||||||
_call("userCourseList", UserCourseAll, user_id, **common)
|
_call("userCourseList", GetUserCourseAll, user_id, **common)
|
||||||
|
|
||||||
if "userChargeList" in wanted:
|
if "userChargeList" in wanted:
|
||||||
_call("userChargeList", UserCharge, user_id, **common)
|
_call("userChargeList", GetUserCharge, user_id, **common)
|
||||||
|
|
||||||
if "userFavoriteList" in wanted:
|
if "userFavoriteList" in wanted:
|
||||||
# ★ 该接口额外带一份**原始** itemKind 副本(_raw),
|
# ★ 该接口额外带一份**原始** itemKind 副本(_raw),
|
||||||
# 供 UpsertUserAll 生成 isNewFavoriteList 位串用
|
# 供 UpsertUserAll 生成 isNewFavoriteList 位串用
|
||||||
# —— 真机 itemKind=10 回的是 null,正是它产生了位串里的 "1"。
|
# —— 真机 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")
|
fav_raw = (raw.get("userFavoriteList") or {}).get("_raw")
|
||||||
if fav_raw:
|
if fav_raw:
|
||||||
raw["_raw_favorite"] = fav_raw
|
raw["_raw_favorite"] = fav_raw
|
||||||
|
|
||||||
if "userActivityList" in wanted:
|
if "userActivityList" in wanted:
|
||||||
_call("userActivityList", UserActivity, user_id, **common)
|
_call("userActivityList", GetUserActivity, user_id, **common)
|
||||||
|
|
||||||
if "userMissionDataList" in wanted:
|
if "userMissionDataList" in wanted:
|
||||||
# ★ 该接口的响应里同时带着 userWeeklyData(周界),
|
# ★ 该接口的响应里同时带着 userWeeklyData(周界),
|
||||||
# 它是 Upsert 侧 userWeeklyData 节点的来源,会被自动提取。
|
# 它是 Upsert 侧 userWeeklyData 节点的来源,会被自动提取。
|
||||||
_call("userMissionDataList", UserMissionData, user_id, **common)
|
_call("userMissionDataList", GetUserMissionData, user_id, **common)
|
||||||
|
|
||||||
if "userIntimateList" in wanted:
|
if "userIntimateList" in wanted:
|
||||||
_call("userIntimateList", UserIntimate, user_id, **common)
|
_call("userIntimateList", GetUserIntimate, user_id, **common)
|
||||||
|
|
||||||
if "userShopItemStockList" in wanted:
|
if "userShopItemStockList" in wanted:
|
||||||
ids = list(shop_item_ids) if shop_item_ids is not None else list(PLAY_SHOP_ITEM_IDS)
|
ids = list(shop_item_ids) if shop_item_ids is not None else list(PLAY_SHOP_ITEM_IDS)
|
||||||
if ids:
|
if ids:
|
||||||
_call("userShopItemStockList", UserShopStock, user_id,
|
_call("userShopItemStockList", GetUserShopStock, user_id,
|
||||||
shop_item_id_list=ids, **common)
|
shop_item_id_list=ids, **common)
|
||||||
else:
|
else:
|
||||||
log_debug("未配置 PLAY_SHOP_ITEM_IDS,跳过商店库存节点")
|
log_debug("未配置 PLAY_SHOP_ITEM_IDS,跳过商店库存节点")
|
||||||
|
|
||||||
if "userFavoritemusicList" in wanted:
|
if "userFavoritemusicList" in wanted:
|
||||||
_call("userFavoritemusicList", UserFavoriteItemAll, user_id, 1, **common)
|
_call("userFavoritemusicList", GetUserFavoriteItemAll, user_id, 1, **common)
|
||||||
|
|
||||||
if "userKaleidxScopeList" in wanted:
|
if "userKaleidxScopeList" in wanted:
|
||||||
_call("userKaleidxScopeList", UserKaleidxScope, user_id, **common)
|
_call("userKaleidxScopeList", GetUserKaleidxScope, user_id, **common)
|
||||||
|
|
||||||
if "userGhost" in wanted:
|
if "userGhost" in wanted:
|
||||||
_call("userGhost", UserGhost, user_id, **common)
|
_call("userGhost", GetUserGhost, user_id, **common)
|
||||||
|
|
||||||
raw["_failed"] = failed
|
raw["_failed"] = failed
|
||||||
log_info("结算快照抓取完成:成功 " + str(len(raw) - 1) + " 个节点,失败 "
|
raw["_userId"] = int(user_id)
|
||||||
|
log_info("结算快照抓取完成:成功 " + str(len(raw) - 2) + " 个节点,失败 "
|
||||||
+ str(len(failed)) + " 个")
|
+ 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
|
return raw
|
||||||
|
|
||||||
|
|
||||||
@@ -281,10 +375,10 @@ if __name__ == "__main__":
|
|||||||
from .log_setup import log_error, setup_logging, teardown_logging
|
from .log_setup import log_error, setup_logging, teardown_logging
|
||||||
|
|
||||||
def _main() -> int:
|
def _main() -> int:
|
||||||
setup_logging("upsert_snapshot_test")
|
setup_logging("get_need_user_data_test")
|
||||||
try:
|
try:
|
||||||
if len(sys.argv) < 2:
|
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
|
return 1
|
||||||
|
|
||||||
user_id = int(sys.argv[1])
|
user_id = int(sys.argv[1])
|
||||||
@@ -302,7 +396,7 @@ if __name__ == "__main__":
|
|||||||
else:
|
else:
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
snap = fetch_upsert_snapshot(user_id, token=token)
|
snap = GetNeedUserData(user_id, token=token)
|
||||||
summary = summarize_snapshot(snap)
|
summary = summarize_snapshot(snap)
|
||||||
print("")
|
print("")
|
||||||
for node, count in summary.items():
|
for node, count in summary.items():
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_activity.py —— 取用户活动记录(GetUserActivityApi)
|
get_user_activity.py —— 取用户活动记录(GetUserActivityApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家的活动/成就记录(playList 与 musicList 两个列表)。
|
取玩家的活动/成就记录(playList 与 musicList 两个列表)。
|
||||||
@@ -16,7 +16,7 @@ user_activity.py —— 取用户活动记录(GetUserActivityApi)
|
|||||||
不是把两个数组首尾相接摊平。
|
不是把两个数组首尾相接摊平。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserActivity
|
from api import GetUserActivity
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -30,7 +30,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserActivity(
|
def GetUserActivity(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_character.py —— 取用户角色列表(GetUserCharacterApi)
|
get_user_character.py —— 取用户角色列表(GetUserCharacterApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家所有角色的等级与觉醒度(真机 312 条)。
|
取玩家所有角色的等级与觉醒度(真机 312 条)。
|
||||||
@@ -14,7 +14,7 @@ user_character.py —— 取用户角色列表(GetUserCharacterApi)
|
|||||||
(真机 source/2026-10-05.log:123 的 userCharacterList 就是这 4 个键)。
|
(真机 source/2026-10-05.log:123 的 userCharacterList 就是这 4 个键)。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserCharacter
|
from api import GetUserCharacter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -28,7 +28,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserCharacter(
|
def GetUserCharacter(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_charge.py —— 取用户票券(GetUserChargeApi)
|
get_user_charge.py —— 取用户票券(GetUserChargeApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家的票券持有情况(真机 3 条)。
|
取玩家的票券持有情况(真机 3 条)。
|
||||||
@@ -13,7 +13,7 @@ user_charge.py —— 取用户票券(GetUserChargeApi)
|
|||||||
★ 回传 Upsert 时要丢掉 extNum1(真机 Upsert 报文里没有这个字段)。
|
★ 回传 Upsert 时要丢掉 extNum1(真机 Upsert 报文里没有这个字段)。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserCharge
|
from api import GetUserCharge
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -27,7 +27,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserCharge(
|
def GetUserCharge(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_course.py —— 取用户课题模式成绩(GetUserCourseApi)
|
get_user_course.py —— 取用户课题模式成绩(GetUserCourseApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家的 COURSE(课题)模式通关记录(真机 16 条)。
|
取玩家的 COURSE(课题)模式通关记录(真机 16 条)。
|
||||||
@@ -12,7 +12,7 @@ user_course.py —— 取用户课题模式成绩(GetUserCourseApi)
|
|||||||
source/2026-10-05.log:55 请求体就是 {"userId":..,"nextIndex":0}
|
source/2026-10-05.log:55 请求体就是 {"userId":..,"nextIndex":0}
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserCourse
|
from api import GetUserCourse
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -23,10 +23,10 @@ from config import CLIENT_ID, TIMEOUT_MS
|
|||||||
|
|
||||||
from .log_setup import log_info
|
from .log_setup import log_info
|
||||||
from .maimai_net import MaimaiNetClient
|
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,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
next_index: int = 0,
|
next_index: int = 0,
|
||||||
@@ -48,7 +48,7 @@ def UserCourse(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def UserCourseAll(
|
def GetUserCourseAll(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -62,7 +62,7 @@ def UserCourseAll(
|
|||||||
{"userCourseList": [...]}
|
{"userCourseList": [...]}
|
||||||
"""
|
"""
|
||||||
log_info("取用户课题成绩(全量):user_id=" + str(user_id))
|
log_info("取用户课题成绩(全量):user_id=" + str(user_id))
|
||||||
items = fetch_all_pages(
|
items = get_all_pages(
|
||||||
"GetUserCourseApi", user_id, "userCourseList",
|
"GetUserCourseApi", user_id, "userCourseList",
|
||||||
body_factory=lambda ni: UserCourseRequestVO(userId=user_id, nextIndex=ni or 0),
|
body_factory=lambda ni: UserCourseRequestVO(userId=user_id, nextIndex=ni or 0),
|
||||||
client_id=client_id, timeout_ms=timeout_ms, client=client,
|
client_id=client_id, timeout_ms=timeout_ms, client=client,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
userdata.py —— 取用户全量数据(GetUserDataApi)
|
get_user_data.py —— 取用户全量数据(GetUserDataApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家档案(86 个字段的大对象),是 upsertUserAll.userData 的**唯一正确来源**。
|
取玩家档案(86 个字段的大对象),是 upsertUserAll.userData 的**唯一正确来源**。
|
||||||
@@ -20,7 +20,7 @@ userdata.py —— 取用户全量数据(GetUserDataApi)
|
|||||||
记录造成的假象(两条响应被合并显示)。
|
记录造成的假象(两条响应被合并显示)。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserData
|
from api import GetUserData
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -34,7 +34,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserData(
|
def GetUserData(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_extend.py —— 取用户扩展设置(GetUserExtendApi)
|
get_user_extend.py —— 取用户扩展设置(GetUserExtendApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家上次的选曲 / 分类 / 卡片槽等扩展状态。
|
取玩家上次的选曲 / 分类 / 卡片槽等扩展状态。
|
||||||
@@ -14,7 +14,7 @@ user_extend.py —— 取用户扩展设置(GetUserExtendApi)
|
|||||||
(真机两处都是 2022),组包时要注意。
|
(真机两处都是 2022),组包时要注意。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserExtend
|
from api import GetUserExtend
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -28,7 +28,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserExtend(
|
def GetUserExtend(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_favorite.py —— 取用户收藏(GetUserFavoriteApi)
|
get_user_favorite.py —— 取用户收藏(GetUserFavoriteApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家各类收藏夹(称号 / 头像 / 相框 / 搭档 / 背景…)。
|
取玩家各类收藏夹(称号 / 头像 / 相框 / 搭档 / 背景…)。
|
||||||
@@ -15,7 +15,7 @@ user_favorite.py —— 取用户收藏(GetUserFavoriteApi)
|
|||||||
位串 isNewFavoriteList 是三值 "0"/"1"/"2"。
|
位串 isNewFavoriteList 是三值 "0"/"1"/"2"。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserFavorite, UserFavoriteAll
|
from api import GetUserFavorite, GetUserFavoriteAll
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -29,7 +29,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserFavoriteRequestVO, FAVORITE_ITEM_KINDS
|
from .user_getter_common import _get, UserFavoriteRequestVO, FAVORITE_ITEM_KINDS
|
||||||
|
|
||||||
|
|
||||||
def UserFavorite(
|
def GetUserFavorite(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
item_kind: int,
|
item_kind: int,
|
||||||
*,
|
*,
|
||||||
@@ -55,7 +55,7 @@ def UserFavorite(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def UserFavoriteAll(
|
def GetUserFavoriteAll(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
item_kinds: Optional[List[int]] = None,
|
item_kinds: Optional[List[int]] = None,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_favorite_item.py —— 取用户收藏曲目/对手(GetUserFavoriteItemApi)
|
get_user_favorite_item.py —— 取用户收藏曲目/对手(GetUserFavoriteItemApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家收藏的乐曲(kind=1)或收藏的对手(kind=2)。
|
取玩家收藏的乐曲(kind=1)或收藏的对手(kind=2)。
|
||||||
@@ -15,7 +15,7 @@ user_favorite_item.py —— 取用户收藏曲目/对手(GetUserFavoriteItemA
|
|||||||
★ 字段名是 kind(不是 itemKind),别写错。
|
★ 字段名是 kind(不是 itemKind),别写错。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserFavoriteItem, UserFavoriteItemAll
|
from api import GetUserFavoriteItem, GetUserFavoriteItemAll
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -26,14 +26,14 @@ from config import CLIENT_ID, TIMEOUT_MS
|
|||||||
|
|
||||||
from .log_setup import log_info
|
from .log_setup import log_info
|
||||||
from .maimai_net import MaimaiNetClient
|
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 取值
|
#: kind 取值
|
||||||
FAVORITE_ITEM_KIND_MUSIC = 1 # 收藏乐曲
|
FAVORITE_ITEM_KIND_MUSIC = 1 # 收藏乐曲
|
||||||
FAVORITE_ITEM_KIND_RIVAL = 2 # 收藏对手
|
FAVORITE_ITEM_KIND_RIVAL = 2 # 收藏对手
|
||||||
|
|
||||||
|
|
||||||
def UserFavoriteItem(
|
def GetUserFavoriteItem(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
kind: int = FAVORITE_ITEM_KIND_MUSIC,
|
kind: int = FAVORITE_ITEM_KIND_MUSIC,
|
||||||
*,
|
*,
|
||||||
@@ -65,7 +65,7 @@ def UserFavoriteItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def UserFavoriteItemAll(
|
def GetUserFavoriteItemAll(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
kind: int = FAVORITE_ITEM_KIND_MUSIC,
|
kind: int = FAVORITE_ITEM_KIND_MUSIC,
|
||||||
*,
|
*,
|
||||||
@@ -81,7 +81,7 @@ def UserFavoriteItemAll(
|
|||||||
{"userFavoriteItemList": [...]}
|
{"userFavoriteItemList": [...]}
|
||||||
"""
|
"""
|
||||||
log_info("取用户收藏项(全量):user_id=" + str(user_id) + " kind=" + str(kind))
|
log_info("取用户收藏项(全量):user_id=" + str(user_id) + " kind=" + str(kind))
|
||||||
items = fetch_all_pages(
|
items = get_all_pages(
|
||||||
"GetUserFavoriteItemApi", user_id, "userFavoriteItemList",
|
"GetUserFavoriteItemApi", user_id, "userFavoriteItemList",
|
||||||
body_factory=lambda ni: UserFavoriteItemRequestVO(
|
body_factory=lambda ni: UserFavoriteItemRequestVO(
|
||||||
userId=user_id, kind=kind, nextIndex=ni or 0,
|
userId=user_id, kind=kind, nextIndex=ni or 0,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_friend_bonus.py —— 取用户好友里程(GetUserFriendBonusApi)
|
get_user_friend_bonus.py —— 取用户好友里程(GetUserFriendBonusApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家本次可领取的好友里程(getMiles)。
|
取玩家本次可领取的好友里程(getMiles)。
|
||||||
@@ -12,7 +12,7 @@ user_friend_bonus.py —— 取用户好友里程(GetUserFriendBonusApi)
|
|||||||
source/2026-10-05.log:111 真机响应 {"returnCode":1,"userId":..,"getMiles":90}
|
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
|
from __future__ import annotations
|
||||||
@@ -26,7 +26,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserFriendBonus(
|
def GetUserFriendBonus(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -53,7 +53,7 @@ def extract_gift_points(friend_bonus: Dict[str, Any]) -> int:
|
|||||||
从好友里程响应里取出 getMiles(本次可领取的里程数)。
|
从好友里程响应里取出 getMiles(本次可领取的里程数)。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
friend_bonus: UserFriendBonus 的返回值
|
friend_bonus: GetUserFriendBonus 的返回值
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
getMiles 整数(取不到则为 0)
|
getMiles 整数(取不到则为 0)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_ghost.py —— 取用户幽灵数据(GetUserGhostApi)
|
get_user_ghost.py —— 取用户幽灵数据(GetUserGhostApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家的「幽灵」对战记录(真机 15 条)。
|
取玩家的「幽灵」对战记录(真机 15 条)。
|
||||||
@@ -11,11 +11,11 @@ user_ghost.py —— 取用户幽灵数据(GetUserGhostApi)
|
|||||||
依据 :source/Assembly-CSharp/Net.VO.Mai2/UserGhostRequestVO.cs
|
依据 :source/Assembly-CSharp/Net.VO.Mai2/UserGhostRequestVO.cs
|
||||||
|
|
||||||
★ Upsert 侧 userGhost 默认留空:源码 VOExtensions.cs:235 是
|
★ 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
|
from __future__ import annotations
|
||||||
@@ -29,7 +29,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserGhost(
|
def GetUserGhost(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_intimate.py —— 取用户搭档亲密度(GetUserIntimateApi)
|
get_user_intimate.py —— 取用户搭档亲密度(GetUserIntimateApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家与各搭档的亲密度(真机 2 条)。
|
取玩家与各搭档的亲密度(真机 2 条)。
|
||||||
@@ -11,7 +11,7 @@ user_intimate.py —— 取用户搭档亲密度(GetUserIntimateApi)
|
|||||||
依据 :source/Assembly-CSharp/Net.VO.Mai2/UserIntimateRequestVO.cs
|
依据 :source/Assembly-CSharp/Net.VO.Mai2/UserIntimateRequestVO.cs
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserIntimate
|
from api import GetUserIntimate
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -25,7 +25,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserIntimate(
|
def GetUserIntimate(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_item.py —— 取用户道具(GetUserItemApi)
|
get_user_item.py —— 取用户道具(GetUserItemApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家各类道具的库存(真机按 kind 分 12 次请求)。
|
取玩家各类道具的库存(真机按 kind 分 12 次请求)。
|
||||||
@@ -29,7 +29,7 @@ source/Assembly-CSharp/Net.Packet.Mai2/PacketGetUserItem.cs:17-24
|
|||||||
后续翻页沿用响应回的 nextIndex
|
后续翻页沿用响应回的 nextIndex
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserItem, UserItemAll
|
from api import GetUserItem, GetUserItemAll
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
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
|
from .user_getter_common import _get, UserItemRequestVO, ITEM_KINDS, ITEM_NEXT_INDEX_FACTOR
|
||||||
|
|
||||||
|
|
||||||
def UserItem(
|
def GetUserItem(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
item_kind: int,
|
item_kind: int,
|
||||||
*,
|
*,
|
||||||
@@ -77,7 +77,7 @@ def UserItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def UserItemAll(
|
def GetUserItemAll(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
item_kinds: Optional[List[int]] = None,
|
item_kinds: Optional[List[int]] = None,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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 取空)
|
★★ 重要区分(曾导致 userData 取空)
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
GameKaleidxScopeResponseVO 里**只有** gameKaleidxScopeList 字段,没有 userData。
|
GameKaleidxScopeResponseVO 里**只有** gameKaleidxScopeList 字段,没有 userData。
|
||||||
玩家档案 userData 的正确来源是 GetUserDataApi(见 userdata.py)。
|
玩家档案 userData 的正确来源是 GetUserDataApi(见 get_user_data.py)。
|
||||||
|
|
||||||
真机日志里「GetGameKaleidxScope 的响应带 userData」是抓包工具把时间相邻的
|
真机日志里「GetGameKaleidxScope 的响应带 userData」是抓包工具把时间相邻的
|
||||||
两条响应合并显示造成的假象 —— 不要据此给 GetGameKaleidxScopeApi 发 userId,
|
两条响应合并显示造成的假象 —— 不要据此给 GetGameKaleidxScopeApi 发 userId,
|
||||||
多发字段会让服务端返回空内容。
|
多发字段会让服务端返回空内容。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserKaleidxScope, GameKaleidxScope
|
from api import GetUserKaleidxScope, GetGameKaleidxScope
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -38,7 +38,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, EmptyRequestVO, UserIdRequestVO
|
from .user_getter_common import _get, EmptyRequestVO, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserKaleidxScope(
|
def GetUserKaleidxScope(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -56,7 +56,7 @@ def UserKaleidxScope(
|
|||||||
client_id=client_id, timeout_ms=timeout_ms, client=client)
|
client_id=client_id, timeout_ms=timeout_ms, client=client)
|
||||||
|
|
||||||
|
|
||||||
def GameKaleidxScope(
|
def GetGameKaleidxScope(
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
timeout_ms: Optional[int] = None,
|
timeout_ms: Optional[int] = None,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_login_bonus.py —— 取用户登录奖励(GetUserLoginBonusApi)
|
get_user_login_bonus.py —— 取用户登录奖励(GetUserLoginBonusApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家的登录奖励领取进度(真机 38 条,分两页)。
|
取玩家的登录奖励领取进度(真机 38 条,分两页)。
|
||||||
@@ -13,7 +13,7 @@ user_login_bonus.py —— 取用户登录奖励(GetUserLoginBonusApi)
|
|||||||
★ 回传 Upsert 时只回传 isCurrent=true 的那一条(本次签到的那条)。
|
★ 回传 Upsert 时只回传 isCurrent=true 的那一条(本次签到的那条)。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserLoginBonus, UserLoginBonusAll
|
from api import GetUserLoginBonus, GetUserLoginBonusAll
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -24,10 +24,10 @@ from config import CLIENT_ID, TIMEOUT_MS
|
|||||||
|
|
||||||
from .log_setup import log_info
|
from .log_setup import log_info
|
||||||
from .maimai_net import MaimaiNetClient
|
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,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
next_index: int = 0,
|
next_index: int = 0,
|
||||||
@@ -52,7 +52,7 @@ def UserLoginBonus(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def UserLoginBonusAll(
|
def GetUserLoginBonusAll(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -66,7 +66,7 @@ def UserLoginBonusAll(
|
|||||||
{"userLoginBonusList": [...]}
|
{"userLoginBonusList": [...]}
|
||||||
"""
|
"""
|
||||||
log_info("取用户登录奖励(全量):user_id=" + str(user_id))
|
log_info("取用户登录奖励(全量):user_id=" + str(user_id))
|
||||||
items = fetch_all_pages(
|
items = get_all_pages(
|
||||||
"GetUserLoginBonusApi", user_id, "userLoginBonusList",
|
"GetUserLoginBonusApi", user_id, "userLoginBonusList",
|
||||||
body_factory=lambda ni: UserLoginBonusRequestVO(
|
body_factory=lambda ni: UserLoginBonusRequestVO(
|
||||||
userId=user_id, nextIndex=ni or 0, maxCount=20),
|
userId=user_id, nextIndex=ni or 0, maxCount=20),
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_map.py —— 取用户地图进度(GetUserMapApi)
|
get_user_map.py —— 取用户地图进度(GetUserMapApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家各地图的推进距离(真机 104 条)。
|
取玩家各地图的推进距离(真机 104 条)。
|
||||||
@@ -14,7 +14,7 @@ user_map.py —— 取用户地图进度(GetUserMapApi)
|
|||||||
(真机:mapId=550003 的 distance 1424000 -> 1435000)。
|
(真机:mapId=550003 的 distance 1424000 -> 1435000)。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserMap / UserMapAll
|
from api import GetUserMap / GetUserMapAll
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -25,10 +25,10 @@ from config import CLIENT_ID, TIMEOUT_MS
|
|||||||
|
|
||||||
from .log_setup import log_info
|
from .log_setup import log_info
|
||||||
from .maimai_net import MaimaiNetClient
|
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,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
next_index: int = 0,
|
next_index: int = 0,
|
||||||
@@ -53,7 +53,7 @@ def UserMap(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def UserMapAll(
|
def GetUserMapAll(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -67,7 +67,7 @@ def UserMapAll(
|
|||||||
{"userMapList": [...]}
|
{"userMapList": [...]}
|
||||||
"""
|
"""
|
||||||
log_info("取用户地图进度(全量):user_id=" + str(user_id))
|
log_info("取用户地图进度(全量):user_id=" + str(user_id))
|
||||||
items = fetch_all_pages(
|
items = get_all_pages(
|
||||||
"GetUserMapApi", user_id, "userMapList",
|
"GetUserMapApi", user_id, "userMapList",
|
||||||
body_factory=lambda ni: UserIdPageRequestVO(
|
body_factory=lambda ni: UserIdPageRequestVO(
|
||||||
userId=user_id, nextIndex=ni or 0, maxCount=1000),
|
userId=user_id, nextIndex=ni or 0, maxCount=1000),
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_mission.py —— 取用户任务与周数据(GetUserMissionDataApi)
|
get_user_mission.py —— 取用户任务与周数据(GetUserMissionDataApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家的任务进度与周常数据(真机 6 条任务)。
|
取玩家的任务进度与周常数据(真机 6 条任务)。
|
||||||
@@ -16,7 +16,7 @@ user_mission.py —— 取用户任务与周数据(GetUserMissionDataApi)
|
|||||||
★ 两者回传 Upsert 时都要丢掉各自的 userId 字段。
|
★ 两者回传 Upsert 时都要丢掉各自的 userId 字段。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserMissionData
|
from api import GetUserMissionData
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -30,7 +30,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserMissionData(
|
def GetUserMissionData(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_music.py —— 取用户曲目成绩(GetUserMusicApi)
|
get_user_music.py —— 取用户曲目成绩(GetUserMusicApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家全部曲目的历史成绩归档(真机 739 组)。
|
取玩家全部曲目的历史成绩归档(真机 739 组)。
|
||||||
@@ -10,7 +10,7 @@ user_music.py —— 取用户曲目成绩(GetUserMusicApi)
|
|||||||
响应 :{userId, nextIndex, userMusicList:[{userMusicDetailList:[...], length}]}
|
响应 :{userId, nextIndex, userMusicList:[{userMusicDetailList:[...], length}]}
|
||||||
依据 :source/Assembly-CSharp/Net.VO.Mai2/UserMusicRequestVO.cs
|
依据 :source/Assembly-CSharp/Net.VO.Mai2/UserMusicRequestVO.cs
|
||||||
source/Assembly-CSharp/Net.VO.Mai2/UserMusic.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 才是
|
★ 响应是**两层嵌套**:外层每元素是一首歌,内层的 userMusicDetailList 才是
|
||||||
按 (musicId, level) 的成绩条目。取数时要摊平。
|
按 (musicId, level) 的成绩条目。取数时要摊平。
|
||||||
@@ -18,7 +18,7 @@ user_music.py —— 取用户曲目成绩(GetUserMusicApi)
|
|||||||
★ 回传 Upsert 时只回传本局玩的那一条(playCount +1),其余不动。
|
★ 回传 Upsert 时只回传本局玩的那一条(playCount +1),其余不动。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserMusicDetail, UserMusicDetailFlat
|
from api import GetUserMusicDetail, UserMusicDetailFlat
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -29,10 +29,10 @@ from config import CLIENT_ID, TIMEOUT_MS
|
|||||||
|
|
||||||
from .log_setup import log_info
|
from .log_setup import log_info
|
||||||
from .maimai_net import MaimaiNetClient
|
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,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
next_index: int = 0,
|
next_index: int = 0,
|
||||||
@@ -57,7 +57,7 @@ def UserMusicDetail(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def UserMusicDetailAll(
|
def GetUserMusicDetailAll(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -72,7 +72,7 @@ def UserMusicDetailAll(
|
|||||||
每元素是**单条 (musicId, level) 成绩**,不是按曲分组的嵌套结构。
|
每元素是**单条 (musicId, level) 成绩**,不是按曲分组的嵌套结构。
|
||||||
"""
|
"""
|
||||||
log_info("取用户曲目成绩(全量):user_id=" + str(user_id))
|
log_info("取用户曲目成绩(全量):user_id=" + str(user_id))
|
||||||
groups = fetch_all_pages(
|
groups = get_all_pages(
|
||||||
"GetUserMusicApi", user_id, "userMusicList",
|
"GetUserMusicApi", user_id, "userMusicList",
|
||||||
body_factory=lambda ni: UserMusicRequestVO(
|
body_factory=lambda ni: UserMusicRequestVO(
|
||||||
userId=user_id, nextIndex=ni or 0, maxCount=50),
|
userId=user_id, nextIndex=ni or 0, maxCount=50),
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_new_item.py —— 取新增道具(GetUserNewItemListApi)
|
get_user_new_item.py —— 取新增道具(GetUserNewItemListApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
上传结算后查询「本次新增了哪些道具」(真机响应 userItemList 为空)。
|
上传结算后查询「本次新增了哪些道具」(真机响应 userItemList 为空)。
|
||||||
@@ -15,7 +15,7 @@ user_new_item.py —— 取新增道具(GetUserNewItemListApi)
|
|||||||
★ 请求体里的 userData / userPlaylogList 需要传,内容与 UpsertUserAll 一致。
|
★ 请求体里的 userData / userPlaylogList 需要传,内容与 UpsertUserAll 一致。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserNewItemList
|
from api import GetUserNewItemList
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -38,7 +38,7 @@ class UserNewItemListRequestVO:
|
|||||||
userPlaylogList: List[Any] = field(default_factory=list)
|
userPlaylogList: List[Any] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
def UserNewItemList(
|
def GetUserNewItemList(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
version: int = PLAY_VERSION_INT,
|
version: int = PLAY_VERSION_INT,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_option.py —— 取用户游戏设置(GetUserOptionApi)
|
get_user_option.py —— 取用户游戏设置(GetUserOptionApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家的音符速度、判定显示、音量等游戏内设置(48 个字段)。
|
取玩家的音符速度、判定显示、音量等游戏内设置(48 个字段)。
|
||||||
@@ -15,7 +15,7 @@ user_option.py —— 取用户游戏设置(GetUserOptionApi)
|
|||||||
裁剪时要丢掉 tempoVolume(真机 Upsert 报文里没有这个字段)。
|
裁剪时要丢掉 tempoVolume(真机 Upsert 报文里没有这个字段)。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserOption
|
from api import GetUserOption
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -29,7 +29,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserOption(
|
def GetUserOption(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
userpreview.py —— 用户预览(GetUserPreviewApi)
|
get_user_preview.py —— 用户预览(GetUserPreviewApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
功能:刷卡后欢迎界面的数据(昵称、Rating、是否已登录、封禁状态…)。
|
功能:刷卡后欢迎界面的数据(昵称、Rating、是否已登录、封禁状态…)。
|
||||||
@@ -17,8 +17,8 @@ userpreview.py —— 用户预览(GetUserPreviewApi)
|
|||||||
(source/Assembly-CSharp/Process.Entry.State/ConfirmPlay.cs:151)。
|
(source/Assembly-CSharp/Process.Entry.State/ConfirmPlay.cs:151)。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserPreview
|
from api import GetUserPreview
|
||||||
pv = UserPreview(12345678, "TESTTOKEN0000000000")
|
pv = GetUserPreview(12345678, "TESTTOKEN0000000000")
|
||||||
print(pv.userName, pv.playerRating)
|
print(pv.userName, pv.playerRating)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ class UserPreviewResponseVO:
|
|||||||
errorId: int = 0
|
errorId: int = 0
|
||||||
|
|
||||||
|
|
||||||
def UserPreview(
|
def GetUserPreview(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
token: str,
|
token: str,
|
||||||
*,
|
*,
|
||||||
@@ -139,10 +139,10 @@ if __name__ == "__main__":
|
|||||||
setup_logging("userpreview_test")
|
setup_logging("userpreview_test")
|
||||||
try:
|
try:
|
||||||
if len(sys.argv) < 3:
|
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
|
return 1
|
||||||
cid = sys.argv[3] if len(sys.argv) > 3 else CLIENT_ID
|
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("")
|
print("")
|
||||||
for name in UserPreviewResponseVO.__dataclass_fields__:
|
for name in UserPreviewResponseVO.__dataclass_fields__:
|
||||||
print(" " + name.ljust(18) + "= " + str(getattr(pv, name)))
|
print(" " + name.ljust(18) + "= " + str(getattr(pv, name)))
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_rating.py —— 取用户 Rating 明细(GetUserRatingApi)
|
get_user_rating.py —— 取用户 Rating 明细(GetUserRatingApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家的 Rating 构成(旧谱 / 新谱 / 下一档候选 + 段位)。
|
取玩家的 Rating 构成(旧谱 / 新谱 / 下一档候选 + 段位)。
|
||||||
@@ -14,7 +14,7 @@ user_rating.py —— 取用户 Rating 明细(GetUserRatingApi)
|
|||||||
★ 回传 Upsert 时是**单元素数组** userRatingList:[{...}],不是对象。
|
★ 回传 Upsert 时是**单元素数组** userRatingList:[{...}],不是对象。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserRating
|
from api import GetUserRating
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -28,7 +28,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserRating(
|
def GetUserRating(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
source/2026-10-05.log:79,81
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserRecommendRateMusic, UserRecommendSelectMusic
|
from api import GetUserRecommendRateMusic, GetUserRecommendSelectMusic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -28,7 +28,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserRecommendRateMusic(
|
def GetUserRecommendRateMusic(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -46,7 +46,7 @@ def UserRecommendRateMusic(
|
|||||||
client_id=client_id, timeout_ms=timeout_ms, client=client)
|
client_id=client_id, timeout_ms=timeout_ms, client=client)
|
||||||
|
|
||||||
|
|
||||||
def UserRecommendSelectMusic(
|
def GetUserRecommendSelectMusic(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
source/2026-10-05.log:77
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserRegion
|
from api import GetUserRegion
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -26,7 +26,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserIdRequestVO
|
from .user_getter_common import _get, UserIdRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserRegion(
|
def GetUserRegion(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
client_id: str = CLIENT_ID,
|
client_id: str = CLIENT_ID,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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 取到)。
|
★ 对手 ID 需要调用方提供(先由 GetUserFavoriteItemApi 的 kind=2 取到)。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserRivalData, UserRivalMusic
|
from api import GetUserRivalData, GetUserRivalMusic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -35,11 +35,11 @@ from .user_getter_common import (
|
|||||||
UserRivalRequestVO,
|
UserRivalRequestVO,
|
||||||
UserRivalMusicRequestVO,
|
UserRivalMusicRequestVO,
|
||||||
RIVAL_MUSIC_LEVELS,
|
RIVAL_MUSIC_LEVELS,
|
||||||
fetch_all_pages,
|
get_all_pages,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def UserRivalData(
|
def GetUserRivalData(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
rival_id: int,
|
rival_id: int,
|
||||||
*,
|
*,
|
||||||
@@ -65,7 +65,7 @@ def UserRivalData(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def UserRivalMusic(
|
def GetUserRivalMusic(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
rival_id: int,
|
rival_id: int,
|
||||||
*,
|
*,
|
||||||
@@ -100,7 +100,7 @@ def UserRivalMusic(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def UserRivalMusicAll(
|
def GetUserRivalMusicAll(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
rival_id: int,
|
rival_id: int,
|
||||||
*,
|
*,
|
||||||
@@ -117,7 +117,7 @@ def UserRivalMusicAll(
|
|||||||
"""
|
"""
|
||||||
lv = list(levels) if levels else list(RIVAL_MUSIC_LEVELS)
|
lv = list(levels) if levels else list(RIVAL_MUSIC_LEVELS)
|
||||||
log_info("取对手曲目成绩(全量):user_id=" + str(user_id) + " rival_id=" + str(rival_id))
|
log_info("取对手曲目成绩(全量):user_id=" + str(user_id) + " rival_id=" + str(rival_id))
|
||||||
items = fetch_all_pages(
|
items = get_all_pages(
|
||||||
"GetUserRivalMusicApi", user_id, "userRivalMusicList",
|
"GetUserRivalMusicApi", user_id, "userRivalMusicList",
|
||||||
body_factory=lambda ni: UserRivalMusicRequestVO(
|
body_factory=lambda ni: UserRivalMusicRequestVO(
|
||||||
userId=user_id, rivalId=rival_id, nextIndex=ni or 0,
|
userId=user_id, rivalId=rival_id, nextIndex=ni or 0,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
user_shop_stock.py —— 取用户商店库存(GetUserShopStockApi)
|
get_user_shop_stock.py —— 取用户商店库存(GetUserShopStockApi)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
取玩家在各商店商品的已兑换次数。
|
取玩家在各商店商品的已兑换次数。
|
||||||
@@ -15,7 +15,7 @@ user_shop_stock.py —— 取用户商店库存(GetUserShopStockApi)
|
|||||||
不传时按空列表发送,服务端返回空 userShopStockList —— 这是合法的。
|
不传时按空列表发送,服务端返回空 userShopStockList —— 这是合法的。
|
||||||
|
|
||||||
调用示例:
|
调用示例:
|
||||||
from api import UserShopStock
|
from api import GetUserShopStock
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -29,7 +29,7 @@ from .maimai_net import MaimaiNetClient
|
|||||||
from .user_getter_common import _get, UserShopStockRequestVO
|
from .user_getter_common import _get, UserShopStockRequestVO
|
||||||
|
|
||||||
|
|
||||||
def UserShopStock(
|
def GetUserShopStock(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
*,
|
*,
|
||||||
shop_item_id_list: Optional[List[int]] = None,
|
shop_item_id_list: Optional[List[int]] = None,
|
||||||
+1
-1
@@ -57,7 +57,7 @@ log_setup.py —— 统一日志模块(INFO / DEBUG 双模式)
|
|||||||
|
|
||||||
from api import log_info, log_debug, log_request, log_response
|
from api import log_info, log_debug, log_request, log_response
|
||||||
|
|
||||||
def UserPreview(user_id, token, ...):
|
def GetUserPreview(user_id, token, ...):
|
||||||
# 函数的第一行 docstring 会成为日志里的「[接口说明]」
|
# 函数的第一行 docstring 会成为日志里的「[接口说明]」
|
||||||
# 例如: '''获取用户预览信息'''
|
# 例如: '''获取用户预览信息'''
|
||||||
log_info(f"请求用户预览 user_id={user_id}") # 可选,想额外记话时才写
|
log_info(f"请求用户预览 user_id={user_id}") # 可选,想额外记话时才写
|
||||||
|
|||||||
+30
-30
@@ -85,16 +85,16 @@ source/2026-10-05.log:123 逐节点核对:
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
五、调用示例
|
五、调用示例
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
from api import (setup_logging, ParseQRCode, UserPreview, UserLogin,
|
from api import (setup_logging, ParseQRCode, GetUserPreview, UserLogin,
|
||||||
fetch_upsert_snapshot, UpsertUserAll, build_play_from_config)
|
GetNeedUserData, UpsertUserAll, build_play_from_config)
|
||||||
|
|
||||||
setup_logging("结算")
|
setup_logging("结算")
|
||||||
user_id, token = ParseQRCode(qr)
|
user_id, token = ParseQRCode(qr)
|
||||||
UserPreview(user_id, token)
|
GetUserPreview(user_id, token)
|
||||||
login = UserLogin(user_id, token)
|
login = UserLogin(user_id, token)
|
||||||
|
|
||||||
# 1) 登录后立刻抓基线快照(差分的基准)
|
# 1) 登录后立刻抓基线快照(差分的基准)
|
||||||
snapshot = fetch_upsert_snapshot(user_id, token=token)
|
snapshot = GetNeedUserData(user_id, token=token)
|
||||||
|
|
||||||
# 2) 组装本局数据(默认读 config.py 的 PLAY_*)
|
# 2) 组装本局数据(默认读 config.py 的 PLAY_*)
|
||||||
play = build_play_from_config()
|
play = build_play_from_config()
|
||||||
@@ -527,7 +527,7 @@ def build_favorite_delta(
|
|||||||
"2" = 服务器侧有对应 itemKind(无论内容是否一致)
|
"2" = 服务器侧有对应 itemKind(无论内容是否一致)
|
||||||
"1" = 服务器侧查不到该 itemKind
|
"1" = 服务器侧查不到该 itemKind
|
||||||
|
|
||||||
★ 关键:**优先用 Get 侧最原始的响应建索引**,不要先用 user_favorite.py
|
★ 关键:**优先用 Get 侧最原始的响应建索引**,不要先用 get_user_favorite.py
|
||||||
的「补 itemKind」逻辑归一化 —— 真机里 itemKind=10 的响应回的是
|
的「补 itemKind」逻辑归一化 —— 真机里 itemKind=10 的响应回的是
|
||||||
{"itemKind":null, "itemIdList":[]},正因为查不到才自然产生 "1"。
|
{"itemKind":null, "itemIdList":[]},正因为查不到才自然产生 "1"。
|
||||||
|
|
||||||
@@ -729,12 +729,12 @@ def derive_play_characters(
|
|||||||
|
|
||||||
数据来源(都在 Get 响应里):
|
数据来源(都在 Get 响应里):
|
||||||
userData.charaSlot 本局 5 个角色槽的 characterId(顺序即 slot1..5)
|
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
|
userCharacterList 每只角色的 level / awakening
|
||||||
GetUserCharacterApi 响应
|
GetUserCharacterApi 响应
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
snapshot: fetch_upsert_snapshot 的返回值(或任何含 userData /
|
snapshot: GetNeedUserData 的返回值(或任何含 userData /
|
||||||
userCharacterList 的字典)。
|
userCharacterList 的字典)。
|
||||||
play: 当前的 PlayData;快照里查不到的槽位回退到它同位置的配置。
|
play: 当前的 PlayData;快照里查不到的槽位回退到它同位置的配置。
|
||||||
|
|
||||||
@@ -811,7 +811,7 @@ def build_post_play_user_data(
|
|||||||
**完全一致**(playCount=1258、point=4460、lastPlayDate=19:22:53.0)。
|
**完全一致**(playCount=1258、point=4460、lastPlayDate=19:22:53.0)。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
游玩后的 userData 字典(可直接放进 [ ] 传给 UserNewItemList)
|
游玩后的 userData 字典(可直接放进 [ ] 传给 GetUserNewItemList)
|
||||||
"""
|
"""
|
||||||
p = play or build_play_from_config()
|
p = play or build_play_from_config()
|
||||||
ua = build_user_all(
|
ua = build_user_all(
|
||||||
@@ -1265,7 +1265,7 @@ def _extract_from_get_responses(raw: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
"userMapList": <GetUserMapApi 的响应>,
|
"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"
|
# GetGameWeeklyDataApi.updateDate = "2026-09-28 04:00:00"
|
||||||
# Upsert 侧 lastLoginWeek = "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;
|
# WeeklyData.LastLoginWeek = gameWeeklyData.updateDate;
|
||||||
# 所以优先用快照里的值;只有拿不到时才退回本地推导。
|
# 所以优先用快照里的值;只有拿不到时才退回本地推导。
|
||||||
if "userWeeklyData" not in out and "userMissionDataList" in raw:
|
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 外层按曲分组,
|
# a) 接口原始响应:两层嵌套(UserMusic.cs 外层按曲分组,
|
||||||
# 内层 userMusicDetailList 才是 (musicId, level) 条目)
|
# 内层 userMusicDetailList 才是 (musicId, level) 条目)
|
||||||
# b) UserMusicDetailAll() 的产物:已摊平成单条
|
# b) GetUserMusicDetailAll() 的产物:已摊平成单条
|
||||||
# 判据:元素里带 "userMusicDetailList" 键 -> 嵌套;否则已摊平。
|
# 判据:元素里带 "userMusicDetailList" 键 -> 嵌套;否则已摊平。
|
||||||
groups = src.get("userMusicList") or []
|
groups = src.get("userMusicList") or []
|
||||||
nested = any(
|
nested = any(
|
||||||
@@ -1382,7 +1382,7 @@ def _extract_from_get_responses(raw: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
items = src.get(list_key)
|
items = src.get(list_key)
|
||||||
# ★ 兜底:取数脚本返回的字典有时用**节点名**作键
|
# ★ 兜底:取数脚本返回的字典有时用**节点名**作键
|
||||||
# (如 UserFavoriteAll -> {"userFavoriteList": [...], "_raw": [...]},
|
# (如 GetUserFavoriteAll -> {"userFavoriteList": [...], "_raw": [...]},
|
||||||
# 而 GetUserFavoriteApi 的原始响应是 {"userFavorite": {...}})。
|
# 而 GetUserFavoriteApi 的原始响应是 {"userFavorite": {...}})。
|
||||||
if items is None:
|
if items is None:
|
||||||
items = src.get(node)
|
items = src.get(node)
|
||||||
@@ -2152,7 +2152,7 @@ def _resolve_api_func(name: str, fallback: Any = None) -> Any:
|
|||||||
★ 为什么要绕这一下:
|
★ 为什么要绕这一下:
|
||||||
autowrap() 只替换 api/__init__.py 命名空间里的名字
|
autowrap() 只替换 api/__init__.py 命名空间里的名字
|
||||||
(见 log_setup.autowrap 的注释),模块内部的直接引用拿到的还是原始函数。
|
(见 log_setup.autowrap 的注释),模块内部的直接引用拿到的还是原始函数。
|
||||||
UpsertUserAll 内部要调 fetch_upsert_snapshot / UserNewItemList / UserData,
|
UpsertUserAll 内部要调 GetNeedUserData / GetUserNewItemList / GetUserData,
|
||||||
绕这一下它们才会像用户直接调用那样打印「=== xxx 开始运行 ===」,
|
绕这一下它们才会像用户直接调用那样打印「=== xxx 开始运行 ===」,
|
||||||
主脚本就完全不用自己写日志了。
|
主脚本就完全不用自己写日志了。
|
||||||
"""
|
"""
|
||||||
@@ -2168,18 +2168,18 @@ def _resolve_api_func(name: str, fallback: Any = None) -> Any:
|
|||||||
|
|
||||||
def _lazy(name: str) -> Any:
|
def _lazy(name: str) -> Any:
|
||||||
"""按名字延迟取 api 子模块里的原始函数(_resolve_api_func 的兜底)。"""
|
"""按名字延迟取 api 子模块里的原始函数(_resolve_api_func 的兜底)。"""
|
||||||
if name == "fetch_upsert_snapshot":
|
if name == "GetNeedUserData":
|
||||||
from .upsert_snapshot import fetch_upsert_snapshot
|
from .get_need_user_data import GetNeedUserData
|
||||||
return fetch_upsert_snapshot
|
return GetNeedUserData
|
||||||
if name == "UserNewItemList":
|
if name == "GetUserNewItemList":
|
||||||
from .user_new_item import UserNewItemList
|
from .get_user_new_item import GetUserNewItemList
|
||||||
return UserNewItemList
|
return GetUserNewItemList
|
||||||
if name == "UserData":
|
if name == "GetUserData":
|
||||||
from .userdata import UserData
|
from .get_user_data import GetUserData
|
||||||
return UserData
|
return GetUserData
|
||||||
if name == "UserMusicDetailAll":
|
if name == "GetUserMusicDetailAll":
|
||||||
from .user_music import UserMusicDetailAll
|
from .get_user_music import GetUserMusicDetailAll
|
||||||
return UserMusicDetailAll
|
return GetUserMusicDetailAll
|
||||||
raise PatchError("未知的内部 api:" + str(name))
|
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 {}
|
before_ud = (base.get("userData") or [{}])[0] if base.get("userData") else {}
|
||||||
|
|
||||||
try:
|
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)
|
timeout_ms=timeout_ms, client=client)
|
||||||
after_ud = (after_raw.get("userData") or {}) if isinstance(after_raw, dict) else {}
|
after_ud = (after_raw.get("userData") or {}) if isinstance(after_raw, dict) else {}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -2245,7 +2245,7 @@ def verify_upsert_landing(
|
|||||||
|
|
||||||
# 本局曲目的 playCount
|
# 本局曲目的 playCount
|
||||||
try:
|
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)
|
timeout_ms=timeout_ms, client=client)
|
||||||
rows = music_raw.get("userMusicList") or [] if isinstance(music_raw, dict) else []
|
rows = music_raw.get("userMusicList") or [] if isinstance(music_raw, dict) else []
|
||||||
hit = [m for m in rows
|
hit = [m for m in rows
|
||||||
@@ -2336,7 +2336,7 @@ def UpsertUserAll(
|
|||||||
auth_date_time: UserLoginApi 请求里的 dateTime(可选;
|
auth_date_time: UserLoginApi 请求里的 dateTime(可选;
|
||||||
userData.dateTime 要用它,真机实测两处一致)
|
userData.dateTime 要用它,真机实测两处一致)
|
||||||
token: 会话令牌(可选)。★ 只在「没传 snapshot 且 auto_snapshot=True」
|
token: 会话令牌(可选)。★ 只在「没传 snapshot 且 auto_snapshot=True」
|
||||||
时需要 —— 内部用它自动调 fetch_upsert_snapshot。
|
时需要 —— 内部用它自动调 GetNeedUserData。
|
||||||
snapshot: 基线快照(可选,见 normalize_snapshot)。
|
snapshot: 基线快照(可选,见 normalize_snapshot)。
|
||||||
传了就用它;不传则按 auto_snapshot 自动抓。
|
传了就用它;不传则按 auto_snapshot 自动抓。
|
||||||
★ 服务器对 userData 是整表覆盖、对差分节点是「只回传变化项」,
|
★ 服务器对 userData 是整表覆盖、对差分节点是「只回传变化项」,
|
||||||
@@ -2390,7 +2390,7 @@ def UpsertUserAll(
|
|||||||
"推荐传 token=...(自动抓)或 snapshot=...(自己抓好传进来)")
|
"推荐传 token=...(自动抓)或 snapshot=...(自己抓好传进来)")
|
||||||
else:
|
else:
|
||||||
log_info("未传 snapshot,自动抓取结算基线快照 ...")
|
log_info("未传 snapshot,自动抓取结算基线快照 ...")
|
||||||
snapshot = _api("fetch_upsert_snapshot")(
|
snapshot = _api("GetNeedUserData")(
|
||||||
user_id, token=token, client_id=client_id,
|
user_id, token=token, client_id=client_id,
|
||||||
place_id=place_id, region_id=region_id, client=client,
|
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,
|
region_id=region_id, region_name=region_name, client_id=client_id,
|
||||||
play_date_time=play_now,
|
play_date_time=play_now,
|
||||||
)
|
)
|
||||||
_api("UserNewItemList")(
|
_api("GetUserNewItemList")(
|
||||||
user_id, version=p.version_int,
|
user_id, version=p.version_int,
|
||||||
user_data=[post_user_data],
|
user_data=[post_user_data],
|
||||||
user_playlog_list=list(user_playlog_list),
|
user_playlog_list=list(user_playlog_list),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ upsert_user_chargelog.py —— 购买功能票 / 上传购买记录(UpsertUse
|
|||||||
source/Assembly-CSharp/Net.VO.Mai2/UserChargelogRequestVO.cs
|
source/Assembly-CSharp/Net.VO.Mai2/UserChargelogRequestVO.cs
|
||||||
public ulong userId;
|
public ulong userId;
|
||||||
public UserChargelog userChargelog;
|
public UserChargelog userChargelog;
|
||||||
public UserCharge userCharge;
|
public GetUserCharge userCharge;
|
||||||
public long loginDateTime;
|
public long loginDateTime;
|
||||||
响应 :{returnCode, apiName}
|
响应 :{returnCode, apiName}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ upsert_user_chargelog.py —— 购买功能票 / 上传购买记录(UpsertUse
|
|||||||
│ └─ 没有 -> 新增一条 stock=1
|
│ └─ 没有 -> 新增一条 stock=1
|
||||||
└─ TicketManager.cs:234 RequestUserCharge(playerId)
|
└─ TicketManager.cs:234 RequestUserCharge(playerId)
|
||||||
├─ VOExtensions.cs:436 ExportEnableUserCharge(ticketId)
|
├─ VOExtensions.cs:436 ExportEnableUserCharge(ticketId)
|
||||||
│ -> 取**购买后**(stock > 0)的那条 UserCharge
|
│ -> 取**购买后**(stock > 0)的那条 GetUserCharge
|
||||||
├─ VOExtensions.cs:403 ExportUserChargelog(ticketId)
|
├─ VOExtensions.cs:403 ExportUserChargelog(ticketId)
|
||||||
│ -> chargeId = ticketId
|
│ -> chargeId = ticketId
|
||||||
│ price = DataManager.GetTicket(ticketId).creditNum ★ 不是 money price
|
│ price = DataManager.GetTicket(ticketId).creditNum ★ 不是 money price
|
||||||
@@ -130,7 +130,7 @@ class ChargelogGuardError(RuntimeError):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UserChargeVO:
|
class UserChargeVO:
|
||||||
"""UserCharge —— 一张票的持有状态(UserCharge.cs,4 字段)。"""
|
"""GetUserCharge —— 一张票的持有状态(GetUserCharge.cs,4 字段)。"""
|
||||||
chargeId: int = 0
|
chargeId: int = 0
|
||||||
stock: int = 0
|
stock: int = 0
|
||||||
purchaseDate: str = ""
|
purchaseDate: str = ""
|
||||||
@@ -349,13 +349,13 @@ def UpsertUserChargelog(
|
|||||||
# ---------- 1. 取当前状态 ----------
|
# ---------- 1. 取当前状态 ----------
|
||||||
if charge_list is None:
|
if charge_list is None:
|
||||||
log_info("未传 charge_list,自动抓取当前票券 ...")
|
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)
|
user_id, client_id=client_id, timeout_ms=timeout_ms, client=client)
|
||||||
charge_list = (raw.get("userChargeList") if isinstance(raw, dict) else None) or []
|
charge_list = (raw.get("userChargeList") if isinstance(raw, dict) else None) or []
|
||||||
|
|
||||||
if user_data is None:
|
if user_data is None:
|
||||||
log_info("未传 user_data,自动抓取当前档案 ...")
|
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_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 {}
|
user_data = (raw_ud.get("userData") if isinstance(raw_ud, dict) else None) or {}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ user_getter_common.py —— 各取数脚本的公共底座
|
|||||||
|
|
||||||
1. 各接口的请求 VO(严格对应 source/.../Net.VO.Mai2/*RequestVO.cs)
|
1. 各接口的请求 VO(严格对应 source/.../Net.VO.Mai2/*RequestVO.cs)
|
||||||
2. _get() 发一次请求并返回**原始响应 dict**
|
2. _get() 发一次请求并返回**原始响应 dict**
|
||||||
3. fetch_all_pages() 自动翻页
|
3. get_all_pages() 自动翻页
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
★ 为什么单独抽出来:各接口的请求体并不统一
|
★ 为什么单独抽出来:各接口的请求体并不统一
|
||||||
@@ -187,7 +187,7 @@ def _get(
|
|||||||
return raw or {}
|
return raw or {}
|
||||||
|
|
||||||
|
|
||||||
def fetch_all_pages(
|
def get_all_pages(
|
||||||
api_name: str,
|
api_name: str,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
list_key: str,
|
list_key: str,
|
||||||
|
|||||||
+7
-7
@@ -6,7 +6,7 @@ buy_ticket.py —— 主脚本范例:扫码登录 → 购买功能票 → 登
|
|||||||
|
|
||||||
一条完整的「买票」链路:
|
一条完整的「买票」链路:
|
||||||
|
|
||||||
ParseQRCode -> UserPreview -> UserLogin -> UpsertUserChargelog -> UserLogout
|
ParseQRCode -> GetUserPreview -> UserLogin -> UpsertUserChargelog -> UserLogout
|
||||||
|
|
||||||
UpsertUserChargelog 内部会自动做这些事(所以主脚本不用管):
|
UpsertUserChargelog 内部会自动做这些事(所以主脚本不用管):
|
||||||
1. 调 GetUserChargeApi 取当前票券列表(算「购买后库存」)
|
1. 调 GetUserChargeApi 取当前票券列表(算「购买后库存」)
|
||||||
@@ -51,17 +51,17 @@ from api import (
|
|||||||
log_error,
|
log_error,
|
||||||
log_info,
|
log_info,
|
||||||
ParseQRCode,
|
ParseQRCode,
|
||||||
UserPreview,
|
GetUserPreview,
|
||||||
UserLogin,
|
UserLogin,
|
||||||
UserLogout,
|
UserLogout,
|
||||||
LogoutType,
|
LogoutType,
|
||||||
UpsertUserChargelog,
|
UpsertUserChargelog,
|
||||||
ChargelogConfirmError,
|
ChargelogConfirmError,
|
||||||
ChargelogGuardError,
|
ChargelogGuardError,
|
||||||
UserCharge,
|
GetUserCharge,
|
||||||
GetGameCharge,
|
GetGameCharge,
|
||||||
ticket_name,
|
ticket_name,
|
||||||
fetch_upsert_snapshot,
|
GetNeedUserData,
|
||||||
summarize_snapshot,
|
summarize_snapshot,
|
||||||
Ping,
|
Ping,
|
||||||
)
|
)
|
||||||
@@ -78,7 +78,7 @@ def show_charges(user_id, label):
|
|||||||
**已经持票还去买,服务端看到的是重复购买,有封号风险。**
|
**已经持票还去买,服务端看到的是重复购买,有封号风险。**
|
||||||
所以买之前必须确认没有持票;买之后要确认状态符合预期。
|
所以买之前必须确认没有持票;买之后要确认状态符合预期。
|
||||||
"""
|
"""
|
||||||
raw = UserCharge(user_id)
|
raw = GetUserCharge(user_id)
|
||||||
lst = (raw.get("userChargeList") if isinstance(raw, dict) else None) or []
|
lst = (raw.get("userChargeList") if isinstance(raw, dict) else None) or []
|
||||||
log_info("[" + label + "] 当前票券 " + str(len(lst)) + " 条:")
|
log_info("[" + label + "] 当前票券 " + str(len(lst)) + " 条:")
|
||||||
if not lst:
|
if not lst:
|
||||||
@@ -164,7 +164,7 @@ def main() -> int:
|
|||||||
user_id, token = ParseQRCode(opts["qr"])
|
user_id, token = ParseQRCode(opts["qr"])
|
||||||
session.update(user_id=user_id, token=token)
|
session.update(user_id=user_id, token=token)
|
||||||
|
|
||||||
preview = UserPreview(user_id, token)
|
preview = GetUserPreview(user_id, token)
|
||||||
if preview.isLogin:
|
if preview.isLogin:
|
||||||
log_error("账号处于已登录状态,请先在机台登出或等待会话超时")
|
log_error("账号处于已登录状态,请先在机台登出或等待会话超时")
|
||||||
return 2
|
return 2
|
||||||
@@ -177,7 +177,7 @@ def main() -> int:
|
|||||||
# -> 13:20:10 才发 UpsertUserChargelogApi
|
# -> 13:20:10 才发 UpsertUserChargelogApi
|
||||||
# 本脚本原来 login 后直接买,跳过了这一段。
|
# 本脚本原来 login 后直接买,跳过了这一段。
|
||||||
if not opts["skip_entry"]:
|
if not opts["skip_entry"]:
|
||||||
snap = fetch_upsert_snapshot(user_id, token=token,
|
snap = GetNeedUserData(user_id, token=token,
|
||||||
place_id=PLACE_ID, region_id=REGION_ID)
|
place_id=PLACE_ID, region_id=REGION_ID)
|
||||||
_sum = summarize_snapshot(snap)
|
_sum = summarize_snapshot(snap)
|
||||||
log_info("入场数据下载完成:" + str(len(_sum)) + " 个节点,失败 "
|
log_info("入场数据下载完成:" + str(len(_sum)) + " 个节点,失败 "
|
||||||
|
|||||||
+410
@@ -0,0 +1,410 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
reincarnate.py —— 账号数据迁移(转生):把账号 A 的数据克隆到账号 B
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
┌─ 源账号 A ─────────────────────────────────────────────┐
|
||||||
|
│ 二维码 -> ParseQRCode -> UserLogin -> GetNeedUserData │
|
||||||
|
│ 或:直接读 data/<userId>.json(离线复用) │
|
||||||
|
└────────────────────────────────────────────────────────┘
|
||||||
|
↓ 节点数据
|
||||||
|
┌─ 目标账号 B ───────────────────────────────────────────┐
|
||||||
|
│ 二维码 -> ParseQRCode -> UserPreview -> UserLogin │
|
||||||
|
│ 循环:UpsertUserAll(每包最多 4 首曲目) │
|
||||||
|
│ 然后 UserLogin(isContinue=true) 续关,拿新 loginId│
|
||||||
|
│ 收尾:UserLogout │
|
||||||
|
└────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
★ 为什么一次最多传 4 首
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
真机一局(一个 credit)最多打 4 首,所以一条 UpsertUserAll 报文里的
|
||||||
|
userPlaylogList / userGamePlaylogList **最多 4 条**。
|
||||||
|
|
||||||
|
本脚本把源账号的 userMusicDetailList 按 4 条一组切分,一组 = 一局。
|
||||||
|
★ 每局都必须带**非空**的 userPlaylogList —— 踩坑清单第 2 条实测:
|
||||||
|
留空会被服务端当成无效包**静默丢弃**(returnCode=1 但不落库)。
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
★ 为什么需要「续关登录」
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
UpsertUserAll 的 playlogId 必须等于**本次登录**的 loginId
|
||||||
|
(PacketUpsertUserAll.cs:31 / UserAllRequestVO)。
|
||||||
|
一局结束后这个会话就被消费掉了,要继续上传必须重新登录 ——
|
||||||
|
真机走的就是「续关」:
|
||||||
|
|
||||||
|
Process/ContinueProcess.cs:403
|
||||||
|
new PacketUserLogin(userID, accessCode, isContinue: true, ...)
|
||||||
|
|
||||||
|
对应本工程:UserLogin(user_id, token, is_continue=True)
|
||||||
|
它和普通登录的唯一区别就是 UserLoginRequestVO.isContinue = true,
|
||||||
|
会返回**新的 loginId / loginDateTime**,供下一局 UpsertUserAll 使用。
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
★ playlog 从哪来
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
GetNeedUserData 抓不到 userPlaylogList(那是逐局记录,没有对应的 Get 接口),
|
||||||
|
所以本脚本**从曲目记录反推合成**:
|
||||||
|
|
||||||
|
UserMusicDetail(9 字段) -> UserPlaylog(110 字段)
|
||||||
|
───────────────────────────── ─────────────────────────
|
||||||
|
musicId musicId
|
||||||
|
level level
|
||||||
|
achievement achievement
|
||||||
|
deluxscoreMax deluxscore
|
||||||
|
scoreRank scoreRank / isClear
|
||||||
|
comboStatus comboStatus
|
||||||
|
syncStatus syncStatus
|
||||||
|
其余字段 取自 config.PLAY_*(合理默认值)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
用法
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
# 源数据来自二维码(会联网抓 A 的数据,并落盘到 data/<A>.json)
|
||||||
|
python reincarnate.py --dst-qr <B的二维码> --src-qr <A的二维码>
|
||||||
|
|
||||||
|
# 源数据来自已保存的 JSON(离线,推荐:不用反复消耗 A 的二维码)
|
||||||
|
python reincarnate.py --dst-qr <B的二维码> --src-json data/12345678.json
|
||||||
|
|
||||||
|
# ★ 真实上传
|
||||||
|
python reincarnate.py --dst-qr <B的二维码> --src-json data/12345678.json --send
|
||||||
|
|
||||||
|
# 只看计划,不发包
|
||||||
|
python reincarnate.py --dst-qr <B的二维码> --src-json data/12345678.json --plan
|
||||||
|
|
||||||
|
# 常用开关
|
||||||
|
--limit N 只迁前 N 首(试跑用)
|
||||||
|
--per-packet N 每包曲目数(默认 4,★ 不要超过 4)
|
||||||
|
--clone-identity 连 userName / accessCode 一起克隆(默认**保留 B 自己的**)
|
||||||
|
--no-continue 不续关(只发一包,调试用)
|
||||||
|
|
||||||
|
★ 迁移会**真实写入**账号 B(不可撤销)。不传 --send 时只构造报文。
|
||||||
|
★ 二维码**有效期 10 分钟**。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from api import (
|
||||||
|
setup_logging,
|
||||||
|
teardown_logging,
|
||||||
|
log_error,
|
||||||
|
log_info,
|
||||||
|
log_warning,
|
||||||
|
ParseQRCode,
|
||||||
|
GetUserPreview,
|
||||||
|
UserLogin,
|
||||||
|
UserLogout,
|
||||||
|
LogoutType,
|
||||||
|
GetNeedUserData,
|
||||||
|
normalize_snapshot,
|
||||||
|
NODE_ORDER,
|
||||||
|
NODE_TYPES,
|
||||||
|
ARRAY_TO_BITS,
|
||||||
|
DIFF_PRIMARY_KEYS,
|
||||||
|
PlayData,
|
||||||
|
build_user_playlog,
|
||||||
|
build_user_game_playlog,
|
||||||
|
UpsertUserAll,
|
||||||
|
ConfirmRequiredError,
|
||||||
|
Ping,
|
||||||
|
load_user_data,
|
||||||
|
list_saved_user_ids,
|
||||||
|
DATA_DIR,
|
||||||
|
)
|
||||||
|
from config import PLACE_ID, REGION_ID
|
||||||
|
|
||||||
|
#: 真机一局最多 4 首
|
||||||
|
MAX_TRACKS_PER_CREDIT = 4
|
||||||
|
|
||||||
|
#: 默认保留目标账号自己的身份字段(不克隆过去,避免把 B 改名)
|
||||||
|
IDENTITY_KEYS = ("userName", "accessCode")
|
||||||
|
|
||||||
|
#: 迁移时**不**逐条传的节点(要么是位串、要么由 UpsertUserAll 自己算)
|
||||||
|
SKIP_NODES = (
|
||||||
|
"userMusicDetailList", # 单独分批
|
||||||
|
"userGamePlaylogList", # 由本脚本按曲目合成
|
||||||
|
"user2pPlaylog", # 2P 记录,迁移无意义
|
||||||
|
"userGetPointList", # 里程/奖励,迁移会凭空发点
|
||||||
|
"userTradeItemList", # 无 Get 来源
|
||||||
|
"userFriendSeasonRankingList", # 无 Get 来源
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv):
|
||||||
|
opts = {"dst_qr": "", "src_qr": "", "src_json": "", "send": False,
|
||||||
|
"plan": False, "debug": False, "limit": 0,
|
||||||
|
"per_packet": MAX_TRACKS_PER_CREDIT,
|
||||||
|
"clone_identity": False, "no_continue": False}
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
a = argv[i]
|
||||||
|
if a == "--send":
|
||||||
|
opts["send"] = True
|
||||||
|
elif a == "--plan":
|
||||||
|
opts["plan"] = True
|
||||||
|
elif a == "--debug":
|
||||||
|
opts["debug"] = True
|
||||||
|
elif a == "--clone-identity":
|
||||||
|
opts["clone_identity"] = True
|
||||||
|
elif a == "--no-continue":
|
||||||
|
opts["no_continue"] = True
|
||||||
|
elif a in ("--dst-qr", "--src-qr", "--src-json", "--limit", "--per-packet") and i + 1 < len(argv):
|
||||||
|
key = a[2:].replace("-", "_")
|
||||||
|
opts[key] = int(argv[i + 1]) if key in ("limit", "per_packet") else argv[i + 1]
|
||||||
|
i += 1
|
||||||
|
i += 1
|
||||||
|
return opts
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 工具
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def _bits_for(node, items, base_items):
|
||||||
|
"""
|
||||||
|
按目标账号 B 的基线算 isNew* 位串:
|
||||||
|
B 里没有该条目 -> "1"(新增)
|
||||||
|
B 里已有 -> "0"(更新)
|
||||||
|
主键表见 upsert_user_all.DIFF_PRIMARY_KEYS。
|
||||||
|
"""
|
||||||
|
keys = DIFF_PRIMARY_KEYS.get(node)
|
||||||
|
if not keys:
|
||||||
|
return None
|
||||||
|
have = set()
|
||||||
|
for b in (base_items or []):
|
||||||
|
if isinstance(b, dict):
|
||||||
|
have.add(tuple(str(b.get(k)) for k in keys))
|
||||||
|
return "".join(
|
||||||
|
"0" if tuple(str(it.get(k)) for k in keys) in have else "1"
|
||||||
|
for it in (items or []) if isinstance(it, dict)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def play_from_detail(detail, base_play=None):
|
||||||
|
"""
|
||||||
|
从一条 UserMusicDetail(9 字段)反推一份 PlayData,用来合成 userPlaylog。
|
||||||
|
|
||||||
|
对不上的字段保留 config.PLAY_* 的默认值(它们不在曲目记录里)。
|
||||||
|
"""
|
||||||
|
p = PlayData() if base_play is None else base_play
|
||||||
|
p.music_id = int(detail.get("musicId", p.music_id) or 0)
|
||||||
|
p.music_level = int(detail.get("level", p.music_level) or 0)
|
||||||
|
p.achievement = int(detail.get("achievement", p.achievement) or 0)
|
||||||
|
p.deluxscore = int(detail.get("deluxscoreMax", p.deluxscore) or 0)
|
||||||
|
rank = int(detail.get("scoreRank", 0) or 0)
|
||||||
|
p.score_rank = rank
|
||||||
|
p.is_clear = rank > 0
|
||||||
|
p.combo_status = int(detail.get("comboStatus", p.combo_status) or 0)
|
||||||
|
p.sync_status = int(detail.get("syncStatus", p.sync_status) or 0)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def collect_nodes(src_nodes, base_nodes, clone_identity):
|
||||||
|
"""把源账号 A 的节点整理成 extra_data(曲目/2P/奖励类除外)。"""
|
||||||
|
out = {}
|
||||||
|
for node in NODE_ORDER:
|
||||||
|
if node in SKIP_NODES:
|
||||||
|
continue
|
||||||
|
if node.startswith("isNew"):
|
||||||
|
continue # 位串单独算
|
||||||
|
kind = NODE_TYPES.get(node)
|
||||||
|
if kind not in ("singleton", "array", "object"):
|
||||||
|
continue
|
||||||
|
val = src_nodes.get(node)
|
||||||
|
if val in (None, [], {}):
|
||||||
|
continue
|
||||||
|
out[node] = json.loads(json.dumps(val)) # 深拷贝
|
||||||
|
# 身份字段:默认保留 B 自己的
|
||||||
|
if not clone_identity and out.get("userData"):
|
||||||
|
for item in out["userData"]:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
for k in IDENTITY_KEYS:
|
||||||
|
item.pop(k, None)
|
||||||
|
# 位串:按 B 的基线算
|
||||||
|
for arr, bits in ARRAY_TO_BITS.items():
|
||||||
|
if arr not in out:
|
||||||
|
continue
|
||||||
|
b = _bits_for(arr, out[arr], base_nodes.get(arr))
|
||||||
|
if b:
|
||||||
|
out[bits] = b
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 主流程
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def fetch_source_by_qr(qr):
|
||||||
|
"""用源账号 A 的二维码联网抓一份数据(并自动落盘到 data/<A>.json)。"""
|
||||||
|
log_info("=== 源账号:联网抓取 ===")
|
||||||
|
uid, token = ParseQRCode(qr)
|
||||||
|
preview = GetUserPreview(uid, token)
|
||||||
|
if preview.isLogin:
|
||||||
|
raise RuntimeError("源账号处于已登录状态,请先在机台登出")
|
||||||
|
login = UserLogin(uid, token, region_id=REGION_ID, place_id=PLACE_ID)
|
||||||
|
try:
|
||||||
|
raw = GetNeedUserData(uid, token=token, place_id=PLACE_ID, region_id=REGION_ID)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
UserLogout(uid, token, login.loginDateTime, logout_type=LogoutType.TestIn,
|
||||||
|
region_id=REGION_ID, place_id=PLACE_ID)
|
||||||
|
except Exception as exc:
|
||||||
|
log_warning("源账号登出失败:" + str(exc))
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
opts = parse_args(sys.argv[1:])
|
||||||
|
if not opts["dst_qr"] or not (opts["src_qr"] or opts["src_json"]):
|
||||||
|
print(__doc__)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if opts["debug"]:
|
||||||
|
import config as _cfg
|
||||||
|
_cfg.LOG_LEVEL = "debug"
|
||||||
|
|
||||||
|
if opts["per_packet"] > MAX_TRACKS_PER_CREDIT:
|
||||||
|
print("--per-packet 不能超过 " + str(MAX_TRACKS_PER_CREDIT) + "(真机一局最多 4 首)")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
setup_logging("reincarnate")
|
||||||
|
session = {"user_id": 0, "token": "", "login_dt": 0, "logged_in": False}
|
||||||
|
try:
|
||||||
|
# ---------------- 1. 源数据 ----------------
|
||||||
|
if opts["src_json"]:
|
||||||
|
src_raw = json.load(open(opts["src_json"], "r", encoding="utf-8"))
|
||||||
|
log_info("=== 源账号:从文件读取 " + opts["src_json"] + " ===")
|
||||||
|
else:
|
||||||
|
src_raw = fetch_source_by_qr(opts["src_qr"])
|
||||||
|
|
||||||
|
src_nodes = normalize_snapshot(src_raw)
|
||||||
|
musics = src_nodes.get("userMusicDetailList") or []
|
||||||
|
if opts["limit"] > 0:
|
||||||
|
musics = musics[:opts["limit"]]
|
||||||
|
log_info("源数据:节点 " + str(len(src_nodes)) + " 个,曲目记录 " + str(len(musics)) + " 条")
|
||||||
|
|
||||||
|
if not musics:
|
||||||
|
log_error("源数据里没有 userMusicDetailList,没什么可迁的。")
|
||||||
|
return 4
|
||||||
|
|
||||||
|
# ---------------- 2. 目标账号 ----------------
|
||||||
|
Ping()
|
||||||
|
uid, token = ParseQRCode(opts["dst_qr"])
|
||||||
|
session.update(user_id=uid, token=token)
|
||||||
|
preview = GetUserPreview(uid, token)
|
||||||
|
if preview.isLogin:
|
||||||
|
log_error("目标账号处于已登录状态,请先在机台登出或等待会话超时")
|
||||||
|
return 2
|
||||||
|
login = UserLogin(uid, token, region_id=REGION_ID, place_id=PLACE_ID)
|
||||||
|
session.update(login_dt=login.loginDateTime, logged_in=True)
|
||||||
|
|
||||||
|
base_raw = GetNeedUserData(uid, token=token, place_id=PLACE_ID, region_id=REGION_ID)
|
||||||
|
base_nodes = normalize_snapshot(base_raw)
|
||||||
|
log_info("目标账号基线已抓取")
|
||||||
|
|
||||||
|
# ---------------- 3. 分批 ----------------
|
||||||
|
per = int(opts["per_packet"])
|
||||||
|
batches = [musics[i:i + per] for i in range(0, len(musics), per)]
|
||||||
|
log_info("计划:共 " + str(len(musics)) + " 首 -> " + str(len(batches))
|
||||||
|
+ " 包(每包 " + str(per) + " 首)")
|
||||||
|
|
||||||
|
if opts["plan"]:
|
||||||
|
for i, b in enumerate(batches):
|
||||||
|
log_info(" 第 " + str(i + 1) + " 包:" + ", ".join(
|
||||||
|
str(x.get("musicId")) + "/L" + str(x.get("level")) for x in b))
|
||||||
|
log_info("--plan:只列计划,不发包")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 第一包才带非曲目节点(整账号克隆)
|
||||||
|
shared_nodes = collect_nodes(src_nodes, base_nodes, opts["clone_identity"])
|
||||||
|
log_info("非曲目节点 " + str(len([k for k in shared_nodes if not k.startswith('isNew')]))
|
||||||
|
+ " 个将在第 1 包上传")
|
||||||
|
|
||||||
|
# ---------------- 4. 逐包上传 ----------------
|
||||||
|
sent = 0
|
||||||
|
for idx, batch in enumerate(batches):
|
||||||
|
log_info("")
|
||||||
|
log_info("========== 第 " + str(idx + 1) + "/" + str(len(batches))
|
||||||
|
+ " 包(" + str(len(batch)) + " 首)==========")
|
||||||
|
|
||||||
|
nodes = {}
|
||||||
|
if idx == 0:
|
||||||
|
nodes.update(json.loads(json.dumps(shared_nodes)))
|
||||||
|
nodes["userMusicDetailList"] = json.loads(json.dumps(batch))
|
||||||
|
b = _bits_for("userMusicDetailList", batch,
|
||||||
|
base_nodes.get("userMusicDetailList"))
|
||||||
|
if b:
|
||||||
|
nodes["isNewMusicDetailList"] = b
|
||||||
|
log_info(" isNewMusicDetailList = " + b)
|
||||||
|
|
||||||
|
# 合成 playlog(每首一条)与 game playlog
|
||||||
|
playlogs = []
|
||||||
|
game_playlogs = []
|
||||||
|
for d in batch:
|
||||||
|
p = play_from_detail(d)
|
||||||
|
playlogs.append(build_user_playlog(
|
||||||
|
login.loginId, login.loginDateTime, p, place_id=PLACE_ID))
|
||||||
|
game_playlogs.append(build_user_game_playlog(
|
||||||
|
login.loginId, login.loginDateTime, p))
|
||||||
|
nodes["userGamePlaylogList"] = game_playlogs
|
||||||
|
|
||||||
|
UpsertUserAll(
|
||||||
|
uid, login.loginId,
|
||||||
|
login_date_time=login.loginDateTime,
|
||||||
|
auth_date_time=login.dateTime,
|
||||||
|
token=token,
|
||||||
|
snapshot=base_raw,
|
||||||
|
auto_characters=False,
|
||||||
|
play=play_from_detail(batch[0]),
|
||||||
|
extra_data=nodes,
|
||||||
|
user_playlog_list=playlogs,
|
||||||
|
pre_new_item_list=False,
|
||||||
|
verify_landing=False,
|
||||||
|
dry_run=not opts["send"],
|
||||||
|
confirm=opts["send"],
|
||||||
|
place_id=PLACE_ID, region_id=REGION_ID,
|
||||||
|
)
|
||||||
|
sent += 1
|
||||||
|
|
||||||
|
# ---- ★ 续关登录:拿下一局的 loginId ----
|
||||||
|
if idx < len(batches) - 1:
|
||||||
|
if opts["no_continue"]:
|
||||||
|
log_warning("--no-continue:不再续关,提前结束(只发了 1 包)")
|
||||||
|
break
|
||||||
|
log_info("--- 续关登录(isContinue=true)---")
|
||||||
|
login = UserLogin(uid, token, region_id=REGION_ID, place_id=PLACE_ID,
|
||||||
|
is_continue=True)
|
||||||
|
session.update(login_dt=login.loginDateTime)
|
||||||
|
|
||||||
|
if not opts["send"]:
|
||||||
|
log_info("")
|
||||||
|
log_info("[dry-run] 未发包。想真实迁移请加 --send(★ 会写入账号 B)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
log_info("")
|
||||||
|
log_info("★ 迁移完成:共上传 " + str(sent) + " 包 / "
|
||||||
|
+ str(min(len(musics), sent * per)) + " 首")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except ConfirmRequiredError:
|
||||||
|
return 5
|
||||||
|
except Exception as exc:
|
||||||
|
log_error(type(exc).__name__ + ": " + str(exc))
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
if session["logged_in"]:
|
||||||
|
try:
|
||||||
|
UserLogout(session["user_id"], session["token"], session["login_dt"],
|
||||||
|
logout_type=LogoutType.TestIn,
|
||||||
|
region_id=REGION_ID, place_id=PLACE_ID)
|
||||||
|
except Exception as exc:
|
||||||
|
log_error("[收尾] 强制登出失败(会话可能需等待服务端超时):" + str(exc))
|
||||||
|
teardown_logging()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+2
-2
@@ -333,7 +333,7 @@ fav_server_raw.append({"itemKind": None, "itemIdList": []})
|
|||||||
_, fav_flags_raw = build_favorite_delta(fav_local, fav_server_raw)
|
_, fav_flags_raw = build_favorite_delta(fav_local, fav_server_raw)
|
||||||
check("itemKind=null 的那条 -> 1(复刻真机 22212)", fav_flags_raw == "22212", fav_flags_raw)
|
check("itemKind=null 的那条 -> 1(复刻真机 22212)", fav_flags_raw == "22212", fav_flags_raw)
|
||||||
|
|
||||||
# 归一化兜底:基线被 user_favorite.py 补齐 itemKind 后,服务器列表里 5 个 kind 齐全,
|
# 归一化兜底:基线被 get_user_favorite.py 补齐 itemKind 后,服务器列表里 5 个 kind 齐全,
|
||||||
# 单看列表已看不出 itemKind=10 原本是 null,必须靠 assume_missing 显式声明。
|
# 单看列表已看不出 itemKind=10 原本是 null,必须靠 assume_missing 显式声明。
|
||||||
fav_server_norm = [{"itemKind": k, "itemIdList": []} for k in (3, 1, 2, 10, 11)]
|
fav_server_norm = [{"itemKind": k, "itemIdList": []} for k in (3, 1, 2, 10, 11)]
|
||||||
_, fav_flags_norm = build_favorite_delta(fav_local, fav_server_norm, (10,))
|
_, fav_flags_norm = build_favorite_delta(fav_local, fav_server_norm, (10,))
|
||||||
@@ -1137,7 +1137,7 @@ check("UpsertUserAll 已被包装", getattr(api.UpsertUserAll, "__mai_logged__",
|
|||||||
check("已挂载 api 数量 >= 40", len(api.get_called_apis()) == 0 or True)
|
check("已挂载 api 数量 >= 40", len(api.get_called_apis()) == 0 or True)
|
||||||
check("会话链路 6 个函数都在",
|
check("会话链路 6 个函数都在",
|
||||||
all(hasattr(api, n) for n in
|
all(hasattr(api, n) for n in
|
||||||
("ParseQRCode", "GetGameSettings", "Ping", "UserPreview",
|
("ParseQRCode", "GetGameSettings", "Ping", "GetUserPreview",
|
||||||
"UserLogin", "UserLogout")))
|
"UserLogin", "UserLogout")))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ from api import (
|
|||||||
log_warning,
|
log_warning,
|
||||||
log_error,
|
log_error,
|
||||||
ParseQRCode,
|
ParseQRCode,
|
||||||
UserPreview,
|
GetUserPreview,
|
||||||
UserLogin,
|
UserLogin,
|
||||||
UserLogout,
|
UserLogout,
|
||||||
LogoutType,
|
LogoutType,
|
||||||
@@ -194,7 +194,7 @@ def main() -> int:
|
|||||||
session.update(user_id=user_id, token=token)
|
session.update(user_id=user_id, token=token)
|
||||||
|
|
||||||
# ---- 预览:顺便挡住「账号已在游戏中」的情况,避免顶号 ----
|
# ---- 预览:顺便挡住「账号已在游戏中」的情况,避免顶号 ----
|
||||||
preview = UserPreview(user_id, token)
|
preview = GetUserPreview(user_id, token)
|
||||||
if preview.isLogin:
|
if preview.isLogin:
|
||||||
log_error("账号处于已登录状态,请先在机台登出或等待会话超时")
|
log_error("账号处于已登录状态,请先在机台登出或等待会话超时")
|
||||||
return 2
|
return 2
|
||||||
|
|||||||
+286
@@ -0,0 +1,286 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
use_ticket.py —— 消耗账号当前持有的功能券(用 UpsertUserAll 表达「用票打了一局」)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
一条完整的「用票」链路:
|
||||||
|
|
||||||
|
ParseQRCode -> GetUserPreview -> UserLogin -> GetUserCharge
|
||||||
|
-> GetNeedUserData(基线快照)-> UpsertUserAll -> GetUserCharge(复查)-> UserLogout
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
★ 为什么可以用 UpsertUserAll 消耗票券
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
GetUserChargeApi 与 UpsertUserAllApi 里的 userChargeList **格式完全相同**
|
||||||
|
(UserCharge.cs:chargeId / stock / purchaseDate / validDate),
|
||||||
|
所以只要把 UpsertUserAll 的 userChargeList 节点改成「该票 stock - 1」,
|
||||||
|
服务端就会认为这张票被这一局用掉了。
|
||||||
|
|
||||||
|
真机实证(gamehook 抓包 2026-10-06 22:23 那一局,正好用了 2 倍券):
|
||||||
|
|
||||||
|
userChargeList: [
|
||||||
|
{"chargeId":2,"stock":0,...}, <- 买来时是 1,本局用掉变成 0
|
||||||
|
{"chargeId":3,"stock":0,...},
|
||||||
|
{"chargeId":5,"stock":0,...}
|
||||||
|
]
|
||||||
|
userGamePlaylogList[0].useTicketId = 2 <- ★ 用了哪张票
|
||||||
|
userGamePlaylogList[0].playCredit = 2 <- ★ 本局消耗的 credit
|
||||||
|
userPlaylogList[0].playCredit = 2
|
||||||
|
userData.lastPlayCredit = 2
|
||||||
|
|
||||||
|
★ playCredit 的算法(source/.../Net.VO.Mai2/VOExtensions.cs:838-845):
|
||||||
|
|
||||||
|
playCredit = Credit.GameCostEnoughPlay() // 投币数,普通局 = 1
|
||||||
|
+ TicketManager.GetTicketCredit(ticketId) // 票的 creditNum
|
||||||
|
|
||||||
|
2 倍券的 creditNum = 1 -> playCredit = 1 + 1 = 2 (与真机抓包一致)
|
||||||
|
creditNum 表见 config.TICKET_CREDIT_NUM。
|
||||||
|
|
||||||
|
★ 真机报文里**没有** isNewChargeList 这个节点(它不在 36 个节点里),
|
||||||
|
所以这里也不需要给位串。
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
用法
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
python use_ticket.py <二维码> # 预检:只看报文,不发包
|
||||||
|
python use_ticket.py <二维码> --list # 只列当前持有的票,不消耗
|
||||||
|
python use_ticket.py <二维码> --ticket-id 2 # 指定要消耗哪张(默认第一张持有的)
|
||||||
|
python use_ticket.py <二维码> --ticket-id 2 --send # ★ 真实消耗
|
||||||
|
python use_ticket.py <二维码> --send --music-id 143 --level 4
|
||||||
|
python use_ticket.py <二维码> --send --skip-entry # 跳过入场数据下载
|
||||||
|
|
||||||
|
★ 消耗会写入服务器(不可撤销)。不传 --send 时只构造报文。
|
||||||
|
★ 二维码**有效期 10 分钟**。
|
||||||
|
★ 账号当前**没有**持有功能票时直接拒绝(没有可消耗的东西)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from api import (
|
||||||
|
setup_logging,
|
||||||
|
teardown_logging,
|
||||||
|
log_error,
|
||||||
|
log_info,
|
||||||
|
log_warning,
|
||||||
|
ParseQRCode,
|
||||||
|
GetUserPreview,
|
||||||
|
UserLogin,
|
||||||
|
UserLogout,
|
||||||
|
LogoutType,
|
||||||
|
GetUserCharge,
|
||||||
|
GetNeedUserData,
|
||||||
|
summarize_snapshot,
|
||||||
|
UpsertUserAll,
|
||||||
|
PlayData,
|
||||||
|
ConfirmRequiredError,
|
||||||
|
ticket_name,
|
||||||
|
Ping,
|
||||||
|
)
|
||||||
|
from config import (
|
||||||
|
PLACE_ID,
|
||||||
|
REGION_ID,
|
||||||
|
PLAY_MUSIC_ID,
|
||||||
|
PLAY_MUSIC_LEVEL,
|
||||||
|
PLAY_CREDIT,
|
||||||
|
TICKET_CREDIT_NUM,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_charges(user_id, label):
|
||||||
|
"""调 GetUserChargeApi,打印票券状态并返回 (全部, 持有的)。"""
|
||||||
|
raw = GetUserCharge(user_id)
|
||||||
|
lst = (raw.get("userChargeList") if isinstance(raw, dict) else None) or []
|
||||||
|
log_info("[" + label + "] 当前票券 " + str(len(lst)) + " 条:")
|
||||||
|
if not lst:
|
||||||
|
log_info(" (空)")
|
||||||
|
for c in lst:
|
||||||
|
cid = c.get("chargeId")
|
||||||
|
stock = int(c.get("stock", 0) or 0)
|
||||||
|
log_info(" chargeId=" + str(cid) + " " + ticket_name(cid)
|
||||||
|
+ " stock=" + str(stock)
|
||||||
|
+ " purchaseDate=" + str(c.get("purchaseDate"))
|
||||||
|
+ " validDate=" + str(c.get("validDate"))
|
||||||
|
+ (" ← ★ 持有中" if stock > 0 else ""))
|
||||||
|
held = [c for c in lst if isinstance(c, dict) and int(c.get("stock", 0) or 0) > 0]
|
||||||
|
log_info("[" + label + "] 持有中的功能票:" + str(len(held)) + " 张")
|
||||||
|
return lst, held
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv):
|
||||||
|
opts = {"qr": "", "ticket_id": None, "send": False, "list": False,
|
||||||
|
"debug": False, "skip_entry": False,
|
||||||
|
"music_id": PLAY_MUSIC_ID, "level": PLAY_MUSIC_LEVEL}
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
a = argv[i]
|
||||||
|
if a == "--send":
|
||||||
|
opts["send"] = True
|
||||||
|
elif a == "--list":
|
||||||
|
opts["list"] = True
|
||||||
|
elif a == "--debug":
|
||||||
|
opts["debug"] = True
|
||||||
|
elif a == "--skip-entry":
|
||||||
|
opts["skip_entry"] = True
|
||||||
|
elif a == "--ticket-id" and i + 1 < len(argv):
|
||||||
|
opts["ticket_id"] = int(argv[i + 1])
|
||||||
|
i += 1
|
||||||
|
elif a == "--music-id" and i + 1 < len(argv):
|
||||||
|
opts["music_id"] = int(argv[i + 1])
|
||||||
|
i += 1
|
||||||
|
elif a == "--level" and i + 1 < len(argv):
|
||||||
|
opts["level"] = int(argv[i + 1])
|
||||||
|
i += 1
|
||||||
|
elif not a.startswith("--") and not opts["qr"]:
|
||||||
|
opts["qr"] = a
|
||||||
|
i += 1
|
||||||
|
return opts
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
opts = parse_args(sys.argv[1:])
|
||||||
|
if not opts["qr"]:
|
||||||
|
print(__doc__)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if opts["debug"]:
|
||||||
|
import config as _cfg
|
||||||
|
_cfg.LOG_LEVEL = "debug"
|
||||||
|
|
||||||
|
setup_logging("use_ticket")
|
||||||
|
session = {"user_id": 0, "token": "", "login_dt": 0, "logged_in": False}
|
||||||
|
try:
|
||||||
|
# ---- 心跳:决定后续所有请求的 number 头 ----
|
||||||
|
Ping()
|
||||||
|
|
||||||
|
user_id, token = ParseQRCode(opts["qr"])
|
||||||
|
session.update(user_id=user_id, token=token)
|
||||||
|
|
||||||
|
preview = GetUserPreview(user_id, token)
|
||||||
|
if preview.isLogin:
|
||||||
|
log_error("账号处于已登录状态,请先在机台登出或等待会话超时")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
login = UserLogin(user_id, token, region_id=REGION_ID, place_id=PLACE_ID)
|
||||||
|
session.update(login_dt=login.loginDateTime, logged_in=True)
|
||||||
|
|
||||||
|
# ---- ★ 读当前持有的功能券 ----
|
||||||
|
all_charges, held = read_charges(user_id, "消耗前")
|
||||||
|
|
||||||
|
if opts["list"]:
|
||||||
|
log_info("--list:只列票券,不做任何写操作")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not held:
|
||||||
|
log_error("账号当前**没有持有任何功能票**,没有可消耗的东西。")
|
||||||
|
log_error(" 想买票请用 buy_ticket.py;已持有的票会在 userChargeList 里 stock > 0。")
|
||||||
|
return 4
|
||||||
|
|
||||||
|
# ---- 选定目标票 ----
|
||||||
|
target = None
|
||||||
|
if opts["ticket_id"] is not None:
|
||||||
|
for c in held:
|
||||||
|
if int(c.get("chargeId", -1)) == int(opts["ticket_id"]):
|
||||||
|
target = c
|
||||||
|
break
|
||||||
|
if target is None:
|
||||||
|
log_error("指定的 chargeId=" + str(opts["ticket_id"])
|
||||||
|
+ " 当前并不持有;持有的有:"
|
||||||
|
+ ", ".join(str(c.get("chargeId")) for c in held))
|
||||||
|
return 4
|
||||||
|
else:
|
||||||
|
target = held[0]
|
||||||
|
|
||||||
|
ticket_id = int(target.get("chargeId"))
|
||||||
|
credit_num = int(TICKET_CREDIT_NUM.get(ticket_id, 0) or 0)
|
||||||
|
play_credit = int(PLAY_CREDIT) + credit_num
|
||||||
|
|
||||||
|
log_info("选定要消耗的票:chargeId=" + str(ticket_id) + " " + ticket_name(ticket_id)
|
||||||
|
+ " stock=" + str(target.get("stock"))
|
||||||
|
+ " creditNum=" + str(credit_num)
|
||||||
|
+ " -> playCredit=" + str(PLAY_CREDIT) + "+" + str(credit_num)
|
||||||
|
+ "=" + str(play_credit))
|
||||||
|
|
||||||
|
# ---- ★ 构造新的 userChargeList:目标票 stock - 1,其余原样 ----
|
||||||
|
new_stock = int(target.get("stock", 0) or 0) - 1
|
||||||
|
if new_stock < 0:
|
||||||
|
new_stock = 0
|
||||||
|
new_list = []
|
||||||
|
for c in all_charges:
|
||||||
|
if not isinstance(c, dict):
|
||||||
|
continue
|
||||||
|
item = dict(c)
|
||||||
|
if int(item.get("chargeId", -1)) == ticket_id:
|
||||||
|
item["stock"] = new_stock
|
||||||
|
log_info(" userChargeList 里 chargeId=" + str(ticket_id)
|
||||||
|
+ " 的 stock:" + str(target.get("stock")) + " -> " + str(new_stock))
|
||||||
|
new_list.append(item)
|
||||||
|
|
||||||
|
# ---- 入场数据下载(贴近真机节奏,同时也是 UpsertUserAll 的基线快照)----
|
||||||
|
snapshot = None
|
||||||
|
if not opts["skip_entry"]:
|
||||||
|
snapshot = GetNeedUserData(user_id, token=token,
|
||||||
|
place_id=PLACE_ID, region_id=REGION_ID)
|
||||||
|
_sum = summarize_snapshot(snapshot)
|
||||||
|
log_info("入场数据下载完成:" + str(len(_sum)) + " 个节点,失败 "
|
||||||
|
+ str(len(snapshot.get("_failed") or [])) + " 个")
|
||||||
|
|
||||||
|
# ---- ★ 用 UpsertUserAll 表达「用这张票打了一局」----
|
||||||
|
# play.use_ticket_id -> userGamePlaylogList[0].useTicketId
|
||||||
|
# play.play_credit -> userGamePlaylogList[0].playCredit / userPlaylogList[].playCredit
|
||||||
|
# / userData.lastPlayCredit
|
||||||
|
# extra_data.userChargeList -> 覆盖节点,把票的 stock 减 1
|
||||||
|
play = PlayData(
|
||||||
|
music_id=opts["music_id"],
|
||||||
|
music_level=opts["level"],
|
||||||
|
use_ticket_id=ticket_id,
|
||||||
|
play_credit=play_credit,
|
||||||
|
)
|
||||||
|
UpsertUserAll(
|
||||||
|
user_id, login.loginId,
|
||||||
|
login_date_time=login.loginDateTime,
|
||||||
|
auth_date_time=login.dateTime,
|
||||||
|
token=token,
|
||||||
|
snapshot=snapshot,
|
||||||
|
play=play,
|
||||||
|
extra_data={"userChargeList": new_list},
|
||||||
|
dry_run=not opts["send"],
|
||||||
|
confirm=opts["send"],
|
||||||
|
verify_landing=bool(opts["send"]),
|
||||||
|
place_id=PLACE_ID, region_id=REGION_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- ★ 复查:票券 stock 应该减 1 ----
|
||||||
|
_, held_after = read_charges(user_id, "消耗后")
|
||||||
|
if not opts["send"]:
|
||||||
|
log_info("[dry-run] 未发包。想真实消耗请加 --send(★ 会写入服务器)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
still = [c for c in held_after if int(c.get("chargeId", -1)) == ticket_id]
|
||||||
|
if still:
|
||||||
|
log_warning("chargeId=" + str(ticket_id) + " 仍然持有(stock="
|
||||||
|
+ str(still[0].get("stock")) + ")—— 服务端可能没落库")
|
||||||
|
return 6
|
||||||
|
log_info("★ 票券已消耗:chargeId=" + str(ticket_id) + " 不再持有 ✅")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except ConfirmRequiredError:
|
||||||
|
return 5
|
||||||
|
except Exception as exc:
|
||||||
|
log_error(type(exc).__name__ + ": " + str(exc))
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
if session["logged_in"]:
|
||||||
|
try:
|
||||||
|
UserLogout(session["user_id"], session["token"], session["login_dt"],
|
||||||
|
logout_type=LogoutType.TestIn,
|
||||||
|
region_id=REGION_ID, place_id=PLACE_ID)
|
||||||
|
except Exception as exc:
|
||||||
|
log_error("[收尾] 强制登出失败(会话可能需等待服务端超时):" + str(exc))
|
||||||
|
teardown_logging()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user