# 功能票使用说明(购买 / 消耗) > 本文是 [`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": , "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)。