重构:Get* 命名统一 + 用户数据落盘 + use_ticket / reincarnate

1) 命名统一(服务端 API 名带 Get 的,脚本名与函数名一律带 get/Get)
   - 26 个 api 脚本改名:userdata.py -> get_user_data.py 等
   - 7 个 game_*.py -> get_game_*.py
   - 38 个函数改名:UserData -> GetUserData 等
   - upsert_snapshot.py -> get_need_user_data.py,
     fetch_upsert_snapshot -> GetNeedUserData
   - user_getter_common.fetch_all_pages -> get_all_pages

2) GetNeedUserData 抓到的数据自动落盘
   - <工程根>/data/<userId>.json,新数据总是覆盖老数据
   - 新增 save_user_data / load_user_data / user_data_path /
     list_saved_user_ids / DATA_DIR / PROJECT_ROOT
   - data/ 已加入 .gitignore

3) use_ticket.py(新)
   - 读取账号当前持有的功能票,用 UpsertUserAll 消耗掉
   - 依据:GetUserChargeApi 与 UpsertUserAllApi 的 userChargeList 格式相同;
     真机实证 userChargeList.stock -1 + useTicketId + playCredit
   - playCredit = 投币数 + 票的 creditNum(VOExtensions.cs:838-845)

4) reincarnate.py(新)
   - 账号数据迁移(转生):A 的数据克隆到 B
   - 每包最多 4 首(真机一局上限),包间用续关登录换新 loginId
     续关依据:ContinueProcess.cs:403 -> PacketUserLogin(isContinue: true)
   - playlog 从曲目记录反推合成(没有对应的 Get 接口)

自检 238 项全绿。
This commit is contained in:
2026-10-06 23:32:09 +08:00
parent c297818d8a
commit 8e3bd6fcaa
46 changed files with 1234 additions and 356 deletions
+81 -11
View File
@@ -25,20 +25,26 @@ WaHappy 是一个用 **Python 纯网络方式**与舞萌DX(SDGB 国服)游
**v0 目前只是一个占位版本,请以「能不能用」而不是「有多少代码」来判断它。**
| 模块 | 状态 |
| 脚本 | 状态 |
|---|---|
| **`upsert.py`** | ✅ **可用** —— 扫码登录 → 上传一局成绩 → 落库回查 → 登出 |
| `buy_ticket.py` | ⚠️ **不可用** —— 购买功能票接口疑似已被服务端拉黑,见下方警告 |
| **`buy_ticket.py`** | ✅ **可用** —— 购买功能票(2026-10-06 真机验证通过) |
| **`use_ticket.py`** | ✅ **可用** —— 读取当前持有的功能票并用 `UpsertUserAll` 消耗掉 |
| **`reincarnate.py`** | 🔧 已实现(未实跑)—— 账号数据迁移(转生),每包 ≤4 首 + 续关登录 |
| `api/` 下 40+ 个接口模块 | 🔧 已实现并通过零网络自检,但**未全部在真机上验证过** |
| **文档** | 📄 在 **`docs` 分支**(`mai` 分支不含)—— 开发过程、踩坑记录、实跑验证,内容比代码本身还多 |
### ⚠️ 关于购买功能票
### 关于购买功能票(已解决)
`buy_ticket.py`(`UpsertUserChargelogApi`)在**报文与真机逐字节一致**的情况下
仍被服务端拒绝(`returnCode=0`),之后该账号**疑似被拉黑,暂时无法再购买功能票**。
`UpsertUserChargelogApi` 曾经一直返回 `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/
├── config.py 运行时可配置项(密钥 / 机台 / 歌曲数据 / 日志级别)
├── config.py 运行时可配置项(密钥 / 机台 / 歌曲数据 / 日志级别)★ 需自建
├── config.py.example 配置模板(机台与店铺标识留空待填)
├── upsert.py ★ 主脚本:扫码登录 → 上传成绩 → 登出
├── buy_ticket.py 主脚本:购买功能票(★ 当前不可用)
├── selftest.py 零网络自检(236 项,判据 = 源码 + 真机日志)
├── buy_ticket.py 主脚本:购买功能票
├── use_ticket.py 主脚本:消耗当前持有的功能票
├── reincarnate.py 主脚本:账号数据迁移(转生)
├── selftest.py 零网络自检(238 项,判据 = 源码 + 真机日志)
│
├── api/ ★ 所有与服务器交互的模块(一个接口一个脚本)
│ ├── __init__.py 统一导出 + 日志自动跟踪
│ ├── maimai_net.py 网络核心(AES / deflate / URL 混淆 / Cookie)
│ ├── log_setup.py 日志(INFO / DEBUG 双模式)
│ ├── upsert_user_all.py 上传成绩(36 节点 / 110 字段 / 差分)
│ ├── upsert_snapshot.py 结算基线快照
│ ├── get_need_user_data.py 结算快照编排 + 落盘 data/<userId>.json
│ └── ... 40+ 个接口脚本
│
├── data/ 每账号一份用户数据快照(<userId>.json)★ 不入库
├── logs/ 日志输出(latest.log + 日期.log)★ 不入库
└── source/ 反编译源码 ★ 不入库,需自备
```
@@ -86,6 +96,9 @@ WaHappy/
**设计约定**:`api/` 里每个脚本 = 一个接口,对外暴露一个**能传参的函数**。
主脚本只负责编排,日志和协议细节都在 `api/` 内部。
**命名约定**:服务端 API 名里带 `Get` 的,**脚本名与函数名一律带 `get` / `Get`**
(不要模棱两可)。所以取数脚本全是 `get_*.py`,函数是 `GetXxx`。
> **`docs/` 在 [`docs` 分支](https://git.fragrance.moe/Txiaochen/wahappy/src/branch/docs),`source/` 与 `logs/` 不入库。**
>
> `source/` 是反编译源码,**需要自备**。
@@ -158,6 +171,63 @@ python upsert.py "<二维码>" --send --no-verify # 跳过上传后的落库
> 二维码从机台的「扫码登录」界面获取,**有效期 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`
真机报文的 36 个节点绝大多数能由快照 + 本局演算自动推出来,