孤儿分支,只含 docs/。内容包括: - 项目总览 / 主脚本编写指南 / 日志指南 - 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条) - 真机 Hook 对照报告 / 实跑验证记录 - 购买功能票流程(含被服务端拒绝的排查过程) · 扫码登录协议技术规范
9.7 KiB
主脚本编写指南
面向「想用这个工程写自己业务流程」的人。 目标:你只写业务,日志和协议细节都交给
api/。
一、三条铁律
1. 不要为每个 api 调用写日志
api/ 里每个函数在导入时都被自动套了日志外壳(log_setup.autowrap),
调用时会自动打印:
=== UserLogin 开始运行 ===
[接口说明] 用户正式登录(开启本次登录会话)。
[位置参数] 12345678, 'TpzUKJM6x1d2iKUP5yLI'
[关键字参数] region_id=1, place_id=1000
[运行结果] 成功 UserLoginResponseVO(8 字段)(耗时 77 ms)
=== UserLogin 运行结束 ===
所以你不需要在调用前后写 log_section / log_info。
主脚本里唯一必需的日志语句只有开头结尾那两句:
setup_logging("我的脚本") # 开头
...
teardown_logging() # 结尾(写在 finally 里)
2. 一定要保证登出
账号若停留在 isLogin=True,下次登录会返回 returnCode=100。
用 try/finally 兜底:
session = {"user_id": 0, "token": "", "login_dt": 0, "logged_in": False}
try:
...
login = UserLogin(user_id, token, ...)
session.update(login_dt=login.loginDateTime, logged_in=True)
...
UserLogout(...)
session["logged_in"] = False
finally:
if session["logged_in"]:
UserLogout(session["user_id"], session["token"], session["login_dt"],
logout_type=LogoutType.TestIn)
3. 有副作用的接口要显式开闸门
UpsertUserAll / UpsertUserChargelog 这类写接口默认不发包,
必须显式传 confirm=True(或 dry_run=True 只看报文):
UpsertUserAll(..., dry_run=True) # 只构造,不发
UpsertUserAll(..., dry_run=False, confirm=True) # 真发
⚠ 只写
dry_run=False不够 ——confirm=None时会去读config.UPSERT_CONFIRM(默认False),照样被拦。
二、最小骨架
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from api import (setup_logging, teardown_logging,
ParseQRCode, UserPreview, UserLogin, UserLogout, LogoutType)
from config import REGION_ID, PLACE_ID
def main() -> int:
setup_logging("我的脚本")
session = {"user_id": 0, "token": "", "login_dt": 0, "logged_in": False}
try:
user_id, token = ParseQRCode(sys.argv[1])
session.update(user_id=user_id, token=token)
UserPreview(user_id, token) # 顶号保护
login = UserLogin(user_id, token,
region_id=REGION_ID, place_id=PLACE_ID)
session.update(login_dt=login.loginDateTime, logged_in=True)
# ---- 你的业务写在这里 ----
UserLogout(user_id, token, login.loginDateTime,
logout_type=LogoutType.TestIn,
region_id=REGION_ID, place_id=PLACE_ID)
session["logged_in"] = False
return 0
finally:
if session["logged_in"]:
try:
UserLogout(session["user_id"], session["token"], session["login_dt"],
logout_type=LogoutType.TestIn,
region_id=REGION_ID, place_id=PLACE_ID)
except Exception:
pass
teardown_logging()
if __name__ == "__main__":
sys.exit(main())
三、日志
3.1 两种模式(改 config.py 一行)
LOG_LEVEL |
输出 |
|---|---|
"info"(默认) |
只展示 api 脚本的运行情况和结果 |
"debug" |
额外展示向 api 发送的数据和 api 返回的数据(含完整报文) |
临时切换(不改 config):
python upsert.py <二维码> --debug
★ DEBUG 会完整打印收发报文,一次结算流程能到 十几 MB。 只想看流程就别开;想瘦身调
config.LOG_VALUE_MAX。
3.2 两个日志文件(同时写)
| 文件 | 模式 | 内容 |
|---|---|---|
logs/latest.log |
覆盖(w) |
只含当次主脚本运行 |
logs/YYYY-MM-DD.log |
追加(a) |
当日全部日志 |
3.3 想记自己的话(可选)
from api import log_info, log_warning, log_error, log_section, log_field
log_section("阶段标题") # 带分隔线
log_info("普通信息")
log_warning("要留意的情况")
log_error("出错了")
log_field("预览数据", preview) # INFO 一行概要;DEBUG 展开完整 JSON
3.4 嵌套调用也会自动记
UpsertUserAll 内部会调 fetch_upsert_snapshot / UserNewItemList / UserData,
这些内部调用同样打印自己的开始/结束行,日志层次完整嵌套:
=== UpsertUserAll 开始运行 ===
=== fetch_upsert_snapshot 开始运行 ===
...
=== fetch_upsert_snapshot 运行结束 ===
=== UserNewItemList 开始运行 ===
...
=== UserNewItemList 运行结束 ===
...
=== UpsertUserAll 运行结束 ===
四、参数约定
| 类型 | 位置 | 例子 |
|---|---|---|
| 业务数据(必需) | 位置参数 | user_id / token / 二维码 / playlog_id / charge_id |
| 配置项(可省) | 关键字参数 | client_id / region_id / place_id / timeout_ms / client |
client 参数:所有 api 都接受一个可选的 MaimaiNetClient。
不传时走全局单例 get_default_client(),连接和 Cookie(JSESSIONID)自动复用。
★ 不要自己
MaimaiNetClient()新建去发任何带 userId 的包源码
Packet.cs:187-194:所有非 Ping 且userId != 0的请求都会Client.AddCookie(Query.UserId)—— 登出也不例外。if (!IsPingPacket()) { Client.AddHeader("number", GameManager.PingMsec.ToString()); if (Query.UserId != 0L) { Client.AddCookie(Query.UserId); } // ← 登出也走这里 }那个 CookieContainer 是
UserLoginApi响应里的Set-Cookie: JSESSIONID=...存下来的(Packet.cs:228-234)。为什么对登出尤其要命:登出请求体里没有 token,只有
userId/accessCode/regionId/placeId/clientId/loginDateTime/type/delayLog。 服务端能用来定位「哪个会话」的,除了userId + loginDateTime,就剩这个 JSESSIONID。 丢掉它有可能登出失败 → 账号卡在isLogin=True(下次登录 returnCode=100)。结论:全程复用同一个 client(不传
client=即默认单例,自动满足)。
五、四个典型流程模板
5.1 只登录(探活 / 查状态)
user_id, token = ParseQRCode(qr)
UserPreview(user_id, token)
login = UserLogin(user_id, token, region_id=REGION_ID, place_id=PLACE_ID)
# ... 用 UserData / UserMusicDetailAll 等取数 ...
5.2 上传一局成绩(最常用)
login = UserLogin(user_id, token, region_id=REGION_ID, place_id=PLACE_ID)
# ★ 一个函数跑完整条链路:抓快照 → 推角色 → 造成绩明细 → 前置接口
# → 组 36 节点 → 合并 extra_data → 自检 → 冷却 → 发包 → 落库回查
UpsertUserAll(
user_id, login.loginId,
login_date_time=login.loginDateTime,
auth_date_time=login.dateTime,
token=token, # 让内部自动抓基线快照
dry_run=False, confirm=True,
)
换歌 / 换成绩只改 config.py 的 PLAY_*,不用动主脚本。
想额外塞节点用 extra_data:
UpsertUserAll(..., extra_data={
"userKaleidxScopeList": [{"gateId": 1, "isGateFound": True, ...}],
"isNewKaleidxScopeList": "1",
})
5.3 买一张功能票
见 buy_ticket.py 与 docs/购买功能票流程.md。核心就一句:
UpsertUserChargelog(user_id, charge_id=2,
login_date_time=login.loginDateTime,
dry_run=False, confirm=True)
5.4 只取数(不发任何写接口)
from api import UserData, UserMusicDetailAll, UserRating
ud = UserData(user_id)
print(ud["userData"]["playCount"])
for m in (UserMusicDetailAll(user_id).get("userMusicList") or []):
if m.get("musicId") == 143:
print(m)
取数类 api 不需要
confirm,也没有副作用,可以随便调。
六、常见错误
| 现象 | 原因 | 解决 |
|---|---|---|
登录 returnCode=100 |
账号卡在 isLogin=True |
上次没登出;等超时或用 finally 兜底 |
UpsertUserAll 抛 ConfirmRequiredError |
只传了 dry_run=False,没传 confirm=True |
补 confirm=True |
扫码 errorID=2 |
二维码过期了。它有时效性(有效期 10 分钟),不是按次数计量的 | 重新生成一个 |
returnCode=1 但数据没变 |
差分节点全空(见踩坑清单坑 2) | 检查 PLAY_* 增量配置 |
| 日志文件特别大 | 开了 DEBUG | 关掉 --debug 或调小 LOG_VALUE_MAX |
RuntimeError: 接口 xxx 返回 0 字节 |
Accept-Encoding 被改过 |
不要动 maimai_net 的该头 |
七、自己写一个新 api
- 在
api/下新建脚本,定义一个能传参的函数 - 函数首行写 docstring(会变成日志的
[接口说明]) - 用
from .maimai_net import request_dict发包 - 用
from .log_setup import log_info记关键结果 - 在
api/__init__.py里导入并加进_API_FUNCS← 漏了这步就没有开始/结束提示
八、相关文档
| 文件 | 内容 |
|---|---|
docs/项目总览.md |
工程结构与 api 清单 |
docs/日志与主脚本指南.md |
日志模块完整参考 |
docs/上传成绩开发文档.md |
UpsertUserAll 的 36 节点 / 110 字段 / 差分语义 |
docs/UpsertUserAll踩坑清单.md |
28 条坑 + 发包前 Checklist |
docs/购买功能票流程.md |
登录 → 买票 → 登出 |
upsert.py |
主脚本范例(极简版) |