孤儿分支,只含 docs/。内容包括: - 项目总览 / 主脚本编写指南 / 日志指南 - 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条) - 真机 Hook 对照报告 / 实跑验证记录 - 购买功能票流程(含被服务端拒绝的排查过程) · 扫码登录协议技术规范
12 KiB
日志与主脚本指南
模块:
api/log_setup.py| 模式:INFO / DEBUG
一、一句话理解
api 调用会被自动识别并记录,主脚本不用写任何日志代码。
主脚本要写的只有两件:
setup_logging("脚本名") # 开头一句
... # 业务代码,照常调 api
teardown_logging() # 结尾一句(不写也会在退出时自动收尾)
二、自动日志长什么样
调用了 ParseQRCode 与 UserPreview,日志自动产出:
==================================================================
日志会话开始 —— main
==================================================================
主脚本 : main
开始时间 : 2026-10-05 17:37:42
日志模式 : INFO(仅运行情况与结果)
Python : 3.12.6 (CPython AMD64)
工作目录 : D:\SDGBSource\杂乱的api\wahappy-v0-goal
已挂载 api : 60 个(自动跟踪,主脚本无需写日志代码)
当次日志 : ...\logs\latest.log
日期日志 : ...\logs\2026-10-05.log
==================================================================
=== ParseQRCode 开始运行 ===
[接口说明] 解析二维码并换取 userId 与 token。
[位置参数] SGWCMAID2610022219463A297AF4CAB863406AA91ED2D29FF3BE20BF1113AC03095D97BD1573EDD691B6
解析二维码登录:chip_id=TESTCLIENT1
发送登录请求:timestamp=261005173742 host=ai.sys-allnet.cn/wc_aime/api/get_data
扫码登录成功:user_id=12345678 token=t3WikM5n...(长度 20)
[运行结果] 成功 tuple(2 项)(耗时 412 ms)
=== ParseQRCode 运行结束 ===
=== UserPreview 开始运行 ===
[接口说明] 获取用户预览信息。
[位置参数] 12345678, 'TESTTOKEN0000000000'
[关键字参数] client_id='TESTCLIENT1'
获取用户预览:user_id=12345678 client_id=TESTCLIENT1
用户预览获取完成:userName=☆Astral☆ playerRating=15000 isLogin=False isNetMember=1
[运行结果] 成功 UserPreviewResponseVO(22 字段)(耗时 388 ms)
=== UserPreview 运行结束 ===
==================================================================
本次运行调用的 api
==================================================================
1. ParseQRCode
2. UserPreview
==================================================================
日志会话结束 —— main
==================================================================
自动带出来的东西:
| 自动内容 | 来源 |
|---|---|
===函数名 开始运行=== / ===函数名 运行结束=== |
导入时自动包装 |
[接口说明] |
函数 docstring 首行 |
[位置参数] / [关键字参数] |
本次调用传入的值 |
[运行结果] |
成功摘要或异常类型,附耗时 |
| 会话结束的 api 汇总清单 | 运行时记录调用顺序 |
三、两种模式
| 模式 | 你能看到什么 |
|---|---|
| INFO(默认) | 开始 → 接口说明 → 参数 → 结果 → 结束 |
| DEBUG | 上面全部 + 请求 URL、请求头、请求体 JSON 全文、状态码、耗时、响应头、响应体 JSON 全文 |
切换方式:改 config.py 一行。
LOG_LEVEL = "info" # 日常运行
LOG_LEVEL = "debug" # 排查协议问题
DEBUG 下额外多出:
-> 请求 GetGameSettingApi
URL : https://maimai-gm.wahlap.com:42081/Maimai2Servlet/mE2s3Jhd/83a5d5a20b062c5c2ad817460b8f7f76
请求头 : {"Content-Type":"application/json","charset":"UTF-8","Mai-Encoding":"1.55","User-Agent":"83a5...#TESTCLIENT1","Accept-Encoding":"","Content-Encoding":"deflate"}
请求体 :
{
"placeId": 1000,
"clientId": "TESTCLIENT1"
}
<- 响应 GetGameSettingApi
状态码 : 200
耗时 : 312 ms
响应头 : {"Date":"...","Content-Type":"application/json"}
响应体 :
{
"isAouAccession": false,
"gameSetting": {
"isMaintenance": false,
"requestInterval": 1200,
...
}
}
DEBUG 模式下完整输出,不折叠、不截断 —— 调试时就是要能逐条核对 1000+ 条曲目、104 条地图。如需限制,改
config.py的LOG_VALUE_MAX。
四、主脚本怎么写
4.1 ★ 核心原则:不要为每个 api 调用写日志
日志全部写在 api 脚本内部。主脚本只需要:
- 开头
setup_logging("脚本名") - 照常调 api 函数
- 结尾
teardown_logging()
不要在每次调用前写 log_section、调用后写 log_info —— 那是多余的,
api 函数自己会打印 === 函数名 开始运行 === / [运行结果] / === 函数名 运行结束 ===。
完整范例见工程根目录的
upsert.py:整条「扫码 → 预览 → 登录 → 结算上传 → 登出」 只有四行业务代码,一行日志都没写。
4.2 最小可用版本
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from api import (setup_logging, teardown_logging,
ParseQRCode, UserPreview, UserLogin, UserLogout, LogoutType)
def main() -> int:
setup_logging("我的脚本") # ← 只需要这一句
try:
user_id, token = ParseQRCode(sys.argv[1])
UserPreview(user_id, token)
login = UserLogin(user_id, token)
UserLogout(user_id, token, login.loginDateTime,
logout_type=LogoutType.TestIn)
return 0
finally:
teardown_logging()
就这些。 没有装饰器,没有埋点,没有 log_section。
4.3 嵌套调用也会自动记日志
UpsertUserAll 内部会调 fetch_upsert_snapshot / UserNewItemList / UserData,
这些内部调用同样会打印自己的开始/结束行(见 upsert_user_all._resolve_api_func),
所以结算链路的日志层次是完整嵌套的:
=== UpsertUserAll 开始运行 ===
[接口说明] 上传一局成绩(UpsertUserAllApi)—— 一个函数跑完整条结算链路。
=== fetch_upsert_snapshot 开始运行 ===
...
=== fetch_upsert_snapshot 运行结束 ===
=== UserNewItemList 开始运行 ===
...
=== UserNewItemList 运行结束 ===
...
=== UpsertUserAll 运行结束 ===
4.4 想额外记点自己的话(可选)
不写也完全不影响:
from api import log_info, log_warning, log_error, log_section, log_field
log_section("阶段标题") # 带分隔线的分节
log_info("普通信息") # 就当成「会进日志文件的 print」
log_warning("需要留意的情况")
log_error("出错了")
log_field("预览数据", preview) # INFO 一行概要;DEBUG 展开完整 JSON
4.5 ★ 保证登出
账号若停留在 isLogin=True,下次登录会 returnCode=100。用这个模板:
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"]:
log_warning("[收尾] 异常中断,强制登出 ...")
try:
UserLogout(session["user_id"], session["token"], session["login_dt"],
logout_type=LogoutType.TestIn)
except Exception as e:
log_error(f"[收尾] 强制登出失败:{e}")
五、api 脚本怎么写
不用加装饰器,直接定义函数就行:
# api/userpreview.py
from .log_setup import log_info, log_debug, log_request, log_response
from .maimai_net import request_dict
def UserPreview(user_id: int, token: str, *, client_id: str = CLIENT_ID):
"""获取用户预览信息。""" # ← 首行成为 [接口说明]
log_info(f"获取用户预览:user_id={user_id}") # 可选
raw = request_dict("GetUserPreviewApi", req, user_id=user_id, client_id=client_id)
# ↑ maimai_net 内部已经调了 log_request / log_response,
# DEBUG 模式下会自动输出收发数据,不用在这里重复写
return parse(raw)
新增 api 必须登记
写完新函数后,在 api/__init__.py 做两步:
# 1) 导入
from .xxx import MyNewApi
# 2) 加进跟踪名单
_API_FUNCS = (
"ParseQRCode",
"UserPreview",
...
"MyNewApi", # ← 加这里
)
autowrap(globals(), _API_FUNCS) 会在导入时自动给它套上日志外壳。
漏了这步,该函数就不会输出 ===开始运行===。
六、完整 API 参考
6.1 会话(主脚本用)
setup_logging(script_name, level=None) -> str
# 初始化会话:确定模式、打开两个日志文件、打印横幅
# level=None 时读 config.LOG_LEVEL
# 同一进程重复调用安全(只初始化一次)
teardown_logging()
# 收尾:打印「本次运行调用的 api」清单 + 结束横幅 + 关文件
# 不调用也会在退出时自动执行
6.2 自动跟踪
autowrap(namespace, names) -> list[str] # api/__init__.py 调用,一般不用手写
get_called_apis() -> list[str] # 本次会话调用过的 api,按首次调用顺序
6.3 模式
get_level() -> "info" | "debug"
set_level(v) -> "info" | "debug"
is_debug() -> bool
LEVEL_INFO = "info"
LEVEL_DEBUG = "debug"
6.4 流程日志(INFO 可见)
log_info("普通信息")
log_step("扫码登录") # → [步骤] 扫码登录
log_step("扫码登录", index=1, total=5) # → [步骤 1/5] 扫码登录
log_result("user_id=123") # → [结果] user_id=123
log_warning("账号已登录") # → [警告] ...
log_error("网络不可达") # → [错误] ...
log_section("阶段标题") # 带分隔线的分节
log_field("预览数据", preview) # INFO 一行;DEBUG 多行展开
log_field("原始响应", text, debug_only=True) # 只在 DEBUG 出现
6.5 收发数据日志(DEBUG 专属)
log_debug("只在 DEBUG 出现")
log_request(url="https://...", body=req, headers=hdr, api_name="XxxApi")
log_response(status=200, body=raw, headers=hdr, api_name="XxxApi", elapsed_ms=412)
log_payload("解密后明文", text)
INFO 模式下这些函数第一行就 return,不做序列化、不拼字符串,可以放心留在热路径里。
6.6 兼容写法
from api import api_log, get_logger
@api_log # 仍可用(会自动跳过重复包装)
def MyFunc(): ...
logger = get_logger()
logger.info("..."); logger.warning("..."); logger.debug("...")
七、日志文件
| 文件 | 打开模式 | 说明 |
|---|---|---|
logs/latest.log |
w(覆盖) |
每次运行重写,只保留最近一次 |
logs/YYYY-MM-DD.log |
a(追加) |
当日所有运行累积,按天分文件 |
两者同时写入。每行都带 [YYYY-MM-DD HH:MM:SS] 时间戳。
DEBUG 的收发数据同样写进文件,可以跑一次 DEBUG 再翻 latest.log。
八、常见问题
Q: 为什么 api 调用没有 ===开始运行===?
A: 该函数没登记在 api/__init__.py 的 _API_FUNCS 里。补上即可。
Q: 为什么 log_debug 什么都没输出?
A: 当前是 INFO 模式。改 config.py 的 LOG_LEVEL = "debug"。
Q: 改了 config.py 没生效?
A: 模式在 setup_logging() 时读取。之前已初始化过就需重启脚本,
或显式 setup_logging("名字", level="debug")。
Q: 主脚本没调用 setup_logging 会怎样?
A: 仍会打印到控制台(自动跟踪在导入时就生效),但不写文件、没有横幅与汇总。
Q: 写测试 / 单独跑 api 脚本时不想要日志?
A: 不调用 setup_logging 就不会开文件;控制台输出仍在。
Q: 能完全关掉吗?
A: 可以 set_level("info") 抑制 DEBUG 部分。要连控制台也静音需改 _emit 的 console 参数。
九、相关文件
| 文件 | 作用 |
|---|---|
api/log_setup.py |
日志模块本体(含自动包装引擎) |
api/__init__.py |
统一导出 + _API_FUNCS 登记表 |
config.py |
LOG_LEVEL / LOG_VALUE_MAX 开关 |
upsert.py |
主脚本范例(含完整的登出兜底) |
api/maimai_net.py |
网络层,已接入 DEBUG 收发数据日志 |