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

381 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 日志与主脚本指南
> 模块:`api/log_setup.py` | 模式:INFO / DEBUG
---
## 一、一句话理解
**api 调用会被自动识别并记录,主脚本不用写任何日志代码。**
主脚本要写的只有两件:
```python
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` 一行。
```python
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 最小可用版本
```python
#!/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 想额外记点自己的话(可选)
不写也完全不影响:
```python
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`。用这个模板:
```python
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 脚本怎么写
**不用加装饰器**,直接定义函数就行:
```python
# 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` 做两步:
```python
# 1) 导入
from .xxx import MyNewApi
# 2) 加进跟踪名单
_API_FUNCS = (
"ParseQRCode",
"UserPreview",
...
"MyNewApi", # ← 加这里
)
```
`autowrap(globals(), _API_FUNCS)` 会在导入时自动给它套上日志外壳。
**漏了这步,该函数就不会输出 `===开始运行===`。**
---
## 六、完整 API 参考
### 6.1 会话(主脚本用)
```python
setup_logging(script_name, level=None) -> str
# 初始化会话:确定模式、打开两个日志文件、打印横幅
# level=None 时读 config.LOG_LEVEL
# 同一进程重复调用安全(只初始化一次)
teardown_logging()
# 收尾:打印「本次运行调用的 api」清单 + 结束横幅 + 关文件
# 不调用也会在退出时自动执行
```
### 6.2 自动跟踪
```python
autowrap(namespace, names) -> list[str] # api/__init__.py 调用,一般不用手写
get_called_apis() -> list[str] # 本次会话调用过的 api,按首次调用顺序
```
### 6.3 模式
```python
get_level() -> "info" | "debug"
set_level(v) -> "info" | "debug"
is_debug() -> bool
LEVEL_INFO = "info"
LEVEL_DEBUG = "debug"
```
### 6.4 流程日志(INFO 可见)
```python
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 专属)
```python
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 兼容写法
```python
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 收发数据日志 |