v0 plus附加功能

- 新增 unlockmusic.py:解锁(解禁)歌曲 + 解禁 Master / Re:Master 难度,整个流程复用 upsert.py 的 run()
- 买票前置闸门 api/ChargeGuard.py:必须先跑过 GetUserCharge,且同进程只允许买一次
- buy_ticket.py 票源改为 GetUserCharge,不再调用 GetGameCharge
- UpsertUserAll 与买票的冷却统一为 15 秒
- api/ 文件名统一 PascalCase(文件名 = 函数名)
- 项目改名 Astral,README 加 Logo、鸣谢与开发者说明
- 自检 284 项
This commit is contained in:
2026-10-17 17:39:55 +08:00
parent b1d7e33c7b
commit 25ac522391
48 changed files with 1137 additions and 348 deletions
+12 -25
View File
@@ -8,11 +8,14 @@ buy_ticket.py —— 主脚本范例:扫码登录 → 购买功能票 → 登
ParseQRCode -> GetUserPreview -> UserLogin -> UpsertUserChargelog -> UserLogout
★ 本脚本判断「能买什么票」**只看 GetUserChargeApi**(账号自己的票券表),
不再调 GetGameChargeApi(那是机台本地的上架清单,跟账号状态无关)。
UpsertUserChargelog 内部会自动做这些事(所以主脚本不用管):
1. 调 GetUserChargeApi 取当前票券列表(算「购买后库存」)
2. 调 GetUserDataApi 取 playCount / playerRating
3. 按 TicketManager.cs:564 算 validDate(现在 -4h +90d,取日期补 04:00:00)
4. 查 TICKET_CREDIT_NUM(已下沉到 api/upsert_user_chargelog.py)得到
4. 查 TICKET_CREDIT_NUM(已下沉到 api/UpsertUserChargelog.py)得到
userChargelog.price(= ticket.creditNum)
5. 闸门 → 发包
@@ -20,7 +23,7 @@ UpsertUserChargelog 内部会自动做这些事(所以主脚本不用管):
用法
--------------------------------------------------------------------------------
python buy_ticket.py <二维码> # 预检:只看报文,不发包
python buy_ticket.py <二维码> --list # 只列当前票券和可买的票
python buy_ticket.py <二维码> --list # 登录后只列当前票券,不购买
python buy_ticket.py <二维码> --charge-id 3 # 指定票(默认 5 = 5倍券)
python buy_ticket.py <二维码> --charge-id 2 --send # ★ 真实购买
python buy_ticket.py <二维码> --charge-id 2 --send --debug
@@ -60,7 +63,6 @@ from api import (
ChargelogConfirmError,
ChargelogGuardError,
GetUserCharge,
GetGameCharge,
ticket_name,
GetNeedUserData,
summarize_snapshot,
@@ -99,7 +101,7 @@ def show_charges(user_id, label):
def parse_args(argv):
opts = {"qr": "", "charge_id": 5, "send": False, "list": False,
"debug": False, "skip_entry": False, "wait_before": 30.0}
"debug": False, "skip_entry": False, "wait_before": 15.0}
i = 0
while i < len(argv):
a = argv[i]
@@ -136,27 +138,6 @@ def main() -> int:
setup_logging("buy_ticket")
session = {"user_id": 0, "token": "", "login_dt": 0, "logged_in": False}
try:
# ---- ★★ 最早的前置检查:这张票当前**上架了吗** ----
# ★ GetGameChargeApi 不需要登录(user_id=0),所以放在**扫码之前**,
# 不满足就直接退出 —— 不浪费二维码,也不产生任何登录会话。
# 依据 TicketManager.cs:137-150:真机也只买 GetGameChargeApi 列出来的票。
shop = GetGameCharge().get("gameChargeList") or []
avail = [int(g.get("chargeId")) for g in shop
if isinstance(g, dict) and g.get("chargeId") is not None]
log_info("当前可购买 chargeId:" + str(avail))
for g in shop:
log_info(" chargeId=" + str(g.get("chargeId"))
+ " " + ticket_name(g.get("chargeId"))
+ " money price=" + str(g.get("price"))
+ " 有效期 " + str(g.get("startDate")) + " ~ " + str(g.get("endDate")))
if opts["charge_id"] not in avail:
log_error("chargeId=" + str(opts["charge_id"])
+ "(" + ticket_name(opts["charge_id"])
+ ")**不在当前可购买清单里** —— 连登录都不做,直接退出。")
log_error(" 原因:服务端对未上架的票会 returnCode=0(实测过)。")
log_error(" 想买请用清单里的 chargeId:" + str(avail))
return 4
# ---- ★ 心跳:决定后续所有请求的 number 头 ----
# 真机序列里 Ping 在 [11],紧接着才是 GetUserPreviewApi [12]。
# 不跑的话 number 恒为 "0"(真机是 75)。
@@ -187,6 +168,10 @@ def main() -> int:
# ---- ★★ 跑前:必须确认账号当前**没有**持有功能票 ----
held_before = show_charges(user_id, "购买前")
if opts["list"]:
log_info("[--list] 只查询票券状态,不购买。")
return 0
aborted = False
if held_before:
log_error("账号当前已持有功能票,**拒绝购买**(重复购买有封号风险):")
@@ -199,6 +184,7 @@ def main() -> int:
aborted = True
else:
# ---- ★ 买票前等待(贴近真机节奏:真机 login 13:19:45 -> 买票 13:20:10,间隔 25s)----
# 默认 15 秒(与 UpsertUserAll 的发包冷却一致)
if opts["wait_before"] > 0:
log_info("买票前等待 " + str(opts["wait_before"]) + " 秒 ...")
time.sleep(opts["wait_before"])
@@ -211,6 +197,7 @@ def main() -> int:
dry_run=not opts["send"],
confirm=opts["send"],
guard_no_stock=True,
guard_purchasable=False, # ★ 不再按 GetGameCharge 上架清单拦截
place_id=PLACE_ID, region_id=REGION_ID,
)