v0版本完成:新增 4 篇详细文档(README 精简内容的落点)

README 为了防冗余做了精简,被移出的详细内容写成文档放这里:

- 功能票使用说明.md      买票/用票的完整原理、报文依据、playCredit 公式、实测记录
- 账号转生说明.md        reincarnate 完整流程、分批规则(避开 2 首)、
                          续关登录依据、登出 type=1 依据、playlog 合成、迁移范围
- 常见坑与注意事项.md    二维码 / 账号状态 / 功能票 / 副作用 / 日志 / 网络 /
                          不入库清单 + .gitignore 行尾注释的坑
- netdelivery说明.md     DLI 协议、amdaemon.exe 字符串出处、用法与实测结论
This commit is contained in:
2026-10-07 11:23:47 +08:00
parent ddacc97b18
commit 3e30dffb31
4 changed files with 576 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
# 功能票使用说明(购买 / 消耗)
> 本文是 [`README.md`](https://git.fragrance.moe/Txiaochen/wahappy) 里功能票部分的**详细版**。
> 购买接口的完整排查过程见 [购买功能票流程.md](购买功能票流程.md) 与
> [购买功能票_存档与交接.md](购买功能票_存档与交接.md)。
---
## 一、票的种类
机台**可购买**的是付费票(`ticketKind=1`,`api.PAID_TICKET_IDS = (2,3,4,5,6)`):
| chargeId | 票 | 上报的 `price`(= 票的 `creditNum`) |
|---|---|---|
| 2 | 2倍チケット | 1 |
| 3 | 3倍チケット | 2 |
| 5 | 5倍チケット | 4 |
> 完整表(含 4/6 倍与活动票)见 `api/upsert_user_chargelog.py` 的 `TICKET_CREDIT_NUM`。
>
> ⚠️ **不同店铺上架的票不一样。** 买之前先 `--list` 看一眼 ——
> 服务端对未上架的票会直接 `returnCode=0`(实测过)。
---
## 二、购买:`buy_ticket.py`
```bash
python buy_ticket.py "<二维码>" --list # 只列当前票券和可买的票
python buy_ticket.py "<二维码>" --charge-id 2 # 预检:只看报文
python buy_ticket.py "<二维码>" --charge-id 2 --send # ★ 真实购买
```
| 参数 | 默认 | 说明 |
|---|---|---|
| `--charge-id N` | **5** | 买哪张票 |
| `--list` | — | 只列票券与可购买清单,不做任何写操作 |
| `--send` | 关 | ★ 真实发包 |
| `--debug` | 关 | 日志里带完整收发报文 |
| `--skip-entry` | 关 | 跳过登录后的「入场全量下载」 |
| `--wait-before N` | **30** | 买票前等待秒数(真机 login→买票是 26 秒) |
### 三道安全检查
1. **买之前**先查 `GetGameChargeApi` —— 目标票**不在当前可购买清单里就直接退出**,
连登录都不做(不浪费二维码)。
依据 `source/Assembly-CSharp/Manager/TicketManager.cs:137-150`。
2. **跑前查一次** `GetUserChargeApi`:**账号已经持有功能票时直接拒绝购买**
(重复购买有封号风险)。想清掉手里的票用 `use_ticket.py`。
3. **跑后再查一次**,确认票券状态符合预期。
### 报文形态
```json
{
"userId": <uid>,
"userChargelog": {
"chargeId": 2, "price": 1,
"purchaseDate": "2026-10-06 22:23:00.0",
"playCount": 1268, "playerRating": 15534,
"placeId": 1007, "regionId": 8, "clientId": "A63E01C3047"
},
"userCharge": {
"chargeId": 2, "stock": 1,
"purchaseDate": "2026-10-06 22:23:00.0",
"validDate": "2027-01-04 04:00:00"
},
"loginDateTime": 1791325354
}
```
| 字段 | 依据 |
|---|---|
| `price` | = `ticket.creditNum`(**不是** `GetGameChargeApi` 里的 money price)。`VOExtensions.cs:410` |
| `stock` | **购买后**的库存(原值 + 1) |
| `purchaseDate` | `TimeManager.DateTimeFormat` = `yyyy-MM-dd HH:mm:ss.f`,**带一位小数** |
| `validDate` | `TicketManager.cs:562-565`:`Now.AddHours(-4).AddDays(90).ToString("yyyy-MM-dd 04:00:00")`,**不带小数** |
> ⚠️ 这两个日期格式不一样 —— `purchaseDate` 带 `.f` 而 `validDate` 不带。
> 少写 `.0` 服务端直接 `returnCode=0`(踩过)。
---
## 三、消耗:`use_ticket.py`
```bash
python use_ticket.py "<二维码>" --list # 只列当前持有的票
python use_ticket.py "<二维码>" --ticket-id 2 # 预检:只看报文
python use_ticket.py "<二维码>" --ticket-id 2 --send # ★ 真实消耗
```
| 参数 | 默认 | 说明 |
|---|---|---|
| `--ticket-id N` | 第一张持有的 | 消耗哪一张 |
| `--list` | — | 只列票券,不做任何写操作 |
| `--send` | 关 | ★ 真实发包 |
| `--music-id N` / `--level N` | `config.PLAY_MUSIC_ID` / `PLAY_MUSIC_LEVEL` | 「这一局」打的歌 |
| `--skip-entry` | 关 | 跳过入场全量下载 |
| `--debug` | 关 | 日志里带完整收发报文 |
### 原理
`GetUserChargeApi` 与 `UpsertUserAllApi` 里的 `userChargeList`
**格式完全相同**(`UserCharge.cs`:`chargeId` / `stock` / `purchaseDate` / `validDate`),
所以只要把该节点里目标票的 `stock` 减 1,再让 playlog 带上
`useTicketId` / `playCredit`,服务端就认为这张票被这一局用掉了。
**真机实证**(2026-10-06 22:23 那局正好用了 2 倍券):
```
userChargeList: chargeId=2 stock 1 -> 0 ← 票被消耗
userGamePlaylogList[0].useTicketId = 2 ← 用了哪张票
userGamePlaylogList[0].playCredit = 2
userPlaylogList[0].playCredit = 2
userData.lastPlayCredit = 2
(报文里**没有** isNewChargeList 这个节点)
```
`playCredit` 的算法(`VOExtensions.cs:838-845`):
```csharp
int num = Credit.GameCostEnoughPlay(); // 投币数,普通局 = 1
int useTicketId = TicketManager.GetUseTicketId(playerId);
int ticketCredit= TicketManager.GetTicketCredit(useTicketId); // = 票的 creditNum
int playCredit = num + ticketCredit;
```
2 倍券的 `creditNum = 1` → `playCredit = 1 + 1 = 2`,与抓包吻合。
### 实测记录(2026-10-07 00:24)
```
[消耗前] chargeId=2 2倍チケット stock=1 ← 持有中
选定要消耗的票:chargeId=2 creditNum=1 -> playCredit=1+1=2
userChargeList 里 chargeId=2 的 stock:1 -> 0
★ 上传成绩成功:returnCode=1
落库回查:
playCount 1269 -> 1269 (PLAY_USER_PLAY_COUNT_GAIN=0,不再 +1)
currentPlayCount 197 -> 197
lastPlayDate 2026-10-06 22:23:56 -> 2026-10-07 00:24:07 ← 落库成功
musicId=143 level=4 playCount -> 11 (曲目次数不变)
★ 票券已消耗:chargeId=2 不再持有 ✅
```
---
## 四、典型用法:票挂在手里,想再买一张
`buy_ticket.py` 检测到**已持票**会拒绝购买。想再买一张:
1. 用 `use_ticket.py --send` 把手里那张消耗掉,或
2. 直接在机台上打一局并选用它
然后再跑 `buy_ticket.py`。
---
## 五、购买接口曾经的坑(摘要)
`UpsertUserChargelogApi` 曾经一直返回 `returnCode=0`,报文与真机**逐字节一致**也照样失败。
**根因:请求头多发了 `Accept` 与 `Connection`。**
真机(Unity Mono 的 `HttpWebRequest`)实际只发这几个头:
```
number / Content-Type / User-Agent / charset / Mai-Encoding / Content-Encoding / Cookie
```
删掉这两个头之后一次通过(`stock` 0 → 1,落库确认)。
> 完整排查过程(12 项已排除假设、账号对照实验、真机抓包对比)见
> [购买功能票流程.md](购买功能票流程.md) 与 [购买功能票_存档与交接.md](购买功能票_存档与交接.md)。