docs: WaHappy 开发文档

孤儿分支,只含 docs/。内容包括:
- 项目总览 / 主脚本编写指南 / 日志指南
- 上传成绩开发文档 / UpsertUserAll 踩坑清单(28 条)
- 真机 Hook 对照报告 / 实跑验证记录
- 购买功能票流程(含被服务端拒绝的排查过程)
  · 扫码登录协议技术规范
This commit is contained in:
Txiaochen
2026-10-06 14:42:32 +08:00
commit 41909b9169
15 changed files with 34059 additions and 0 deletions
+380
View File
@@ -0,0 +1,380 @@
# 日志与主脚本指南
> 模块:`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 收发数据日志 |