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

6.4 KiB
Raw Blame History

功能票使用说明(购买 / 消耗)

本文是 README.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

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. 跑后再查一次,确认票券状态符合预期。

报文形态

{
  "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

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):

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。