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
+156 -3
View File
@@ -27,6 +27,8 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import api
import unlockmusic
import upsert
from api import (
BITS_TO_ARRAY,
JUDGE_BOXES,
@@ -38,6 +40,7 @@ from api import (
build_delta,
build_favorite_delta,
build_play_from_config,
build_unlock_items,
build_user_all,
build_user_game_playlog,
build_user_playlog,
@@ -49,6 +52,13 @@ from api import (
UpsertUserChargelog,
ChargelogConfirmError,
ChargelogGuardError,
has_fresh_read,
held_charges,
last_read_age,
note_charge_purchase,
note_user_charge_read,
purchased_charge_ids,
reset_charge_guard,
ticket_name,
ticket_price,
calc_valid_date,
@@ -70,13 +80,13 @@ from api import (
login_date_time_now,
local_utc_offset_seconds,
)
from api.upsert_user_all import (
from api.UpsertUserAll import (
ConfirmRequiredError,
PatchError,
UpsertUserAll,
UserAllRequestVO,
)
from api.upsert_user_chargelog import _now_string
from api.UpsertUserChargelog import _now_string
# ★ 这些常量原先在 config.py,2026-10-06 已下沉到各 api 模块 —— 从 api 取。
from api import (
@@ -351,7 +361,7 @@ fav_server_raw.append({"itemKind": None, "itemIdList": []})
_, fav_flags_raw = build_favorite_delta(fav_local, fav_server_raw)
check("itemKind=null 的那条 -> 1(复刻真机 22212)", fav_flags_raw == "22212", fav_flags_raw)
# 归一化兜底:基线被 get_user_favorite.py 补齐 itemKind 后,服务器列表里 5 个 kind 齐全,
# 归一化兜底:基线被 GetUserFavorite.py 补齐 itemKind 后,服务器列表里 5 个 kind 齐全,
# 单看列表已看不出 itemKind=10 原本是 null,必须靠 assume_missing 显式声明。
fav_server_norm = [{"itemKind": k, "itemIdList": []} for k in (3, 1, 2, 10, 11)]
_, fav_flags_norm = build_favorite_delta(fav_local, fav_server_norm, (10,))
@@ -1102,6 +1112,149 @@ except ChargelogConfirmError:
check("买票未开闸门抛 ChargelogConfirmError(未发包)", True)
# =============================================================================
# 13.7 歌曲解锁(解禁)
# =============================================================================
section("13.7 歌曲解锁(解禁)")
# 依据 source/Assembly-CSharp/Net.VO.Mai2/VOExtensions.cs:981-1008
# kind5 = MusicUnlockList(歌曲本体)
# kind6 = MusicUnlockMasterList(Master)
# kind7 = MusicUnlockReMasterList(Re:Master)
# 依据 source/Assembly-CSharp/Manager/NotesListManager.cs:415/455
# `if (id < 10000) return true;` -> 标准谱 Master/Re:Master 天生解锁
_unl_std, _unl_std_notes = build_unlock_items([1085])
check("标准谱(<10000)只写 kind5,不写 kind6/7",
[it["itemKind"] for it in _unl_std] == [5], str([it["itemKind"] for it in _unl_std]))
check("解锁条目形态 = {itemKind,itemId,stock,isValid}",
_unl_std[0] == {"itemKind": 5, "itemId": 1085, "stock": 1, "isValid": True},
str(_unl_std[0]))
check("标准谱给出「天生解锁」说明",
any("天生解锁" in n for n in _unl_std_notes), str(_unl_std_notes))
_unl_dx, _ = build_unlock_items([11414])
check("DX 谱写 kind5 + kind6 + kind7",
[it["itemKind"] for it in _unl_dx] == [5, 6, 7], str([it["itemKind"] for it in _unl_dx]))
_unl_m, _ = build_unlock_items([11414], master=False)
check("master=False 不写 kind6", [it["itemKind"] for it in _unl_m] == [5, 7],
str([it["itemKind"] for it in _unl_m]))
_unl_r, _ = build_unlock_items([11414], remaster=False)
check("remaster=False 不写 kind7", [it["itemKind"] for it in _unl_r] == [5, 6],
str([it["itemKind"] for it in _unl_r]))
_unl_dup, _ = build_unlock_items([11414], existing=[{"itemKind": 5, "itemId": 11414}])
check("已存在的条目自动跳过(不重复插入)",
[it["itemKind"] for it in _unl_dup] == [6, 7], str([it["itemKind"] for it in _unl_dup]))
_unl_all, _ = build_unlock_items(
[11414], existing=[{"itemKind": k, "itemId": 11414} for k in (5, 6, 7)])
check("全都有时返回空列表", _unl_all == [], str(_unl_all))
check("musicId 重复只写一次", len(build_unlock_items([1085, 1085])[0]) == 1)
_ua_unl = build_user_all(baseline, login_date_time=1791159576, playlog_id=1,
unlock_music_ids=[11414])
check("build_user_all 把解锁条目追加进 userItemList",
[it["itemKind"] for it in _ua_unl["userItemList"]] == [5, 6, 7],
str(_ua_unl["userItemList"]))
check("解锁条目的位串全是 1(新增)", _ua_unl["isNewItemList"] == "111",
_ua_unl["isNewItemList"])
check("位串长度与 userItemList 条数一致",
len(_ua_unl["isNewItemList"]) == len(_ua_unl["userItemList"]))
check("不给 unlock_music_ids 时不产生 userItem 条目",
build_user_all(baseline, login_date_time=1791159576,
playlog_id=1)["userItemList"] == [])
_ua_unl3 = build_user_all(
baseline, login_date_time=1791159576, playlog_id=1,
extra_data={"userItemList": [{"itemKind": 5, "itemId": 1085,
"stock": 1, "isValid": True}],
"isNewItemList": "1"},
unlock_music_ids=[11414])
check("extra_data 给了 userItemList 时解锁条目追加在其后",
[it["itemId"] for it in _ua_unl3["userItemList"]] == [1085, 11414, 11414, 11414],
str([it["itemId"] for it in _ua_unl3["userItemList"]]))
check("已有条目的位串保留、新条目补 1", _ua_unl3["isNewItemList"] == "1111",
_ua_unl3["isNewItemList"])
# =============================================================================
# 13.8 unlockmusic.py 的参数解析(主脚本复用 upsert.run)
# =============================================================================
section("13.8 unlockmusic.py 参数解析")
_uopts = unlockmusic.parse_args(["QRCODE", "--music", "11414", "--send"])
check("--music 解析成 int 列表", _uopts["unlock_music"] == [11414],
str(_uopts["unlock_music"]))
check("二维码取第一个非 -- 参数", _uopts["qr"] == "QRCODE", _uopts["qr"])
check("--send 置位", _uopts["send"] is True)
_uopts = unlockmusic.parse_args(["QR", "--music", "1085,11414", "--music", "7"])
check("逗号(含中文逗号)+ 可重复都认",
_uopts["unlock_music"] == [1085, 11414, 7], str(_uopts["unlock_music"]))
_uopts = unlockmusic.parse_args(["QR", "--unlock-music", "11414",
"--no-unlock-master", "--no-unlock-remaster"])
check("--unlock-music 是 --music 的别名", _uopts["unlock_music"] == [11414])
check("两个难度开关默认 True、可关掉",
_uopts["unlock_master"] is False and _uopts["unlock_remaster"] is False)
_uopts = unlockmusic.parse_args(["QR", "--music", "abc"])
check("非法 musicId 记进 unlock_bad", _uopts.get("unlock_bad") == "abc")
check("upsert.parse_args 不再认识 --unlock-music",
"unlock_music" not in upsert.parse_args(["QR", "--unlock-music", "11414"]))
check("upsert.run 存在且可被别的脚本复用", callable(getattr(upsert, "run", None)))
# =============================================================================
# 13.9 买票前置闸门(必须先跑 GetUserCharge)
# =============================================================================
section("13.9 买票前置闸门(必须先跑 GetUserCharge)")
reset_charge_guard()
check("没查过票时 has_fresh_read 为 False", has_fresh_read(55555) is False)
check("没查过票时 last_read_age 为 None", last_read_age(55555) is None)
note_user_charge_read(55555, [{"chargeId": 2, "stock": 0},
{"chargeId": 5, "stock": 1}])
check("查过之后 has_fresh_read 为 True", has_fresh_read(55555) is True)
check("held_charges 只挑 stock>0 的",
[c["chargeId"] for c in held_charges(55555)] == [5], str(held_charges(55555)))
try:
UpsertUserChargelog(66666, 2, 1791159576, user_data={},
guard_purchasable=False, dry_run=True)
check("没查票就买票应抛 ChargelogGuardError", False, "未抛异常")
except ChargelogGuardError as exc:
check("没查票就买票抛 ChargelogGuardError(未发包)", True)
check("报错信息里点明要先调 GetUserCharge",
"GetUserCharge" in str(exc), str(exc)[:120])
check("显式传 charge_list 视为已查票(可通过)",
UpsertUserChargelog(66666, 2, 1791159576, charge_list=[], user_data={},
guard_purchasable=False,
dry_run=True).userCharge.chargeId == 2)
note_charge_purchase(77777, 2)
check("purchased_charge_ids 记录已买", purchased_charge_ids(77777) == [2])
note_user_charge_read(77777, [])
try:
UpsertUserChargelog(77777, 2, 1791159576, charge_list=[], user_data={},
guard_purchasable=False, dry_run=True)
check("同进程重复买票应被拦下", False, "未抛异常")
except ChargelogGuardError as exc:
check("同进程重复买票抛 ChargelogGuardError", True)
check("报错信息点明已经买过", "已经买过" in str(exc), str(exc)[:120])
reset_charge_guard()
check("reset 之后记忆清空",
has_fresh_read(55555) is False and purchased_charge_ids(77777) == [])
# =============================================================================
# 14. 副作用闸门
# =============================================================================