Files
Astral/docs/日志与主脚本指南.md
T
Txiaochen 41909b9169 docs: WaHappy 开发文档
孤儿分支,只含 docs/。内容包括:
- 项目总览 / 主脚本编写指南 / 日志指南
- 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条)
- 真机 Hook 对照报告 / 实跑验证记录
- 购买功能票流程(含被服务端拒绝的排查过程)
  · 扫码登录协议技术规范
2026-10-06 14:42:32 +08:00

12 KiB
Raw Blame History

日志与主脚本指南

模块: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 脚本内部。主脚本只需要:

  1. 开头 setup_logging("脚本名")
  2. 照常调 api 函数
  3. 结尾 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 收发数据日志