Files
Astral/docs/功能票使用说明.md
T
Txiaochen 3e30dffb31 v0版本完成:新增 4 篇详细文档(README 精简内容的落点)
README 为了防冗余做了精简,被移出的详细内容写成文档放这里:

- 功能票使用说明.md      买票/用票的完整原理、报文依据、playCredit 公式、实测记录
- 账号转生说明.md        reincarnate 完整流程、分批规则(避开 2 首)、
                          续关登录依据、登出 type=1 依据、playlog 合成、迁移范围
- 常见坑与注意事项.md    二维码 / 账号状态 / 功能票 / 副作用 / 日志 / 网络 /
                          不入库清单 + .gitignore 行尾注释的坑
- netdelivery说明.md     DLI 协议、amdaemon.exe 字符串出处、用法与实测结论
2026-10-07 11:23:47 +08:00

174 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 功能票使用说明(购买 / 消耗)
> 本文是 [`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)。