Files
soul-yongping/scripts/test/miniapp/test_article_preview_speed.py
Alex-larget 46f94a9c81 在多个页面中通过骨架屏优化加载状态。
在章节、礼物代付详情、阅读和搜索结果页面,用骨架屏替换传统加载指示器,以提升数据获取过程中的用户体验。
更新骨架屏样式,使加载状态更加美观。
实现章节和配置信息的缓存策略,以优化性能并减少冷启动问题。
2026-03-18 12:56:34 +08:00

235 lines
8.1 KiB
Python
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.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文章阅读与界面预览 GET 接口响应速度测试
测试范围:
- 界面预览config、book/parts、book/all-chapters、book/chapters-by-part
- 文章阅读book/chapter/:id、book/chapter/by-mid/:mid
用法:
SOUL_TEST_ENV=soulapi python scripts/test/miniapp/test_article_preview_speed.py
SOUL_TEST_ENV=soulapi python -m scripts.test.miniapp.test_article_preview_speed
产出:控制台报表 + 开发文档/测试报告-文章阅读与界面预览响应速度-YYYYMMDD.md
"""
import json
import sys
import time
from pathlib import Path
import requests
# 加载测试配置
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from config import API_BASE, ENV_LABEL, get_env_banner
# 每接口请求次数(取平均)
ROUNDS = 5
TIMEOUT = 30
def measure_get(url: str, desc: str) -> dict:
"""对 GET 请求测速,返回 {ok, status_code, times_ms, avg_ms, min_ms, max_ms, error}"""
times_ms = []
last_error = None
last_status = None
for _ in range(ROUNDS):
t0 = time.perf_counter()
try:
r = requests.get(url, timeout=TIMEOUT)
last_status = r.status_code
elapsed = (time.perf_counter() - t0) * 1000
times_ms.append(elapsed)
if r.status_code != 200:
last_error = f"HTTP {r.status_code}"
except requests.RequestException as e:
last_error = str(e)
times_ms.append(-1)
if not times_ms:
return {"ok": False, "error": last_error or "无响应", "status_code": last_status}
valid = [t for t in times_ms if t >= 0]
return {
"ok": len(valid) == ROUNDS and (last_status or 200) == 200,
"status_code": last_status,
"times_ms": times_ms,
"avg_ms": sum(valid) / len(valid) if valid else 0,
"min_ms": min(valid) if valid else 0,
"max_ms": max(valid) if valid else 0,
"error": last_error,
}
def main():
print(get_env_banner())
base = API_BASE.rstrip("/")
# 1. 先拉取 parts 和 all-chapters获取 partId、id、mid
parts_url = f"{base}/api/miniprogram/book/parts"
all_chapters_url = f"{base}/api/miniprogram/book/all-chapters"
parts_data = None
all_chapters_data = None
try:
r = requests.get(parts_url, timeout=TIMEOUT)
if r.status_code == 200:
parts_data = r.json()
except Exception:
pass
try:
r = requests.get(all_chapters_url, timeout=TIMEOUT)
if r.status_code == 200:
all_chapters_data = r.json()
except Exception:
pass
part_id = None
chapter_id = None
chapter_mid = None
if parts_data and parts_data.get("success"):
parts = parts_data.get("parts") or []
fixed = parts_data.get("fixedSections") or []
if parts:
part_id = parts[0].get("id")
if fixed:
chapter_mid = fixed[0].get("mid")
chapter_id = fixed[0].get("id")
if (not chapter_id or not chapter_mid) and all_chapters_data and all_chapters_data.get("success"):
arr = all_chapters_data.get("data") or all_chapters_data.get("chapters") or []
if arr:
first = arr[0] if isinstance(arr[0], dict) else {}
chapter_id = chapter_id or first.get("id")
chapter_mid = chapter_mid or first.get("mid")
if not part_id and parts_data and parts_data.get("success"):
parts = parts_data.get("parts") or []
if parts:
part_id = parts[0].get("id")
# 2. 定义测试用例(仅 GET
cases = [
("界面预览-配置", f"{base}/api/miniprogram/config", "GET /api/miniprogram/config"),
("界面预览-目录", f"{base}/api/miniprogram/book/parts", "GET /api/miniprogram/book/parts"),
("界面预览-全书章节", f"{base}/api/miniprogram/book/all-chapters", "GET /api/miniprogram/book/all-chapters"),
]
if part_id:
cases.append(
(
"界面预览-篇章内章节",
f"{base}/api/miniprogram/book/chapters-by-part?partId={part_id}",
f"GET /api/miniprogram/book/chapters-by-part?partId={part_id}",
)
)
if chapter_id:
cases.append(
(
"文章阅读-按id",
f"{base}/api/miniprogram/book/chapter/{chapter_id}",
f"GET /api/miniprogram/book/chapter/:id",
)
)
if chapter_mid:
cases.append(
(
"文章阅读-按mid",
f"{base}/api/miniprogram/book/chapter/by-mid/{chapter_mid}",
f"GET /api/miniprogram/book/chapter/by-mid/:mid",
)
)
# 3. 执行测速
results = []
for name, url, api_desc in cases:
print(f"\n测速: {name} ({api_desc})")
res = measure_get(url, name)
res["name"] = name
res["api"] = api_desc
res["url"] = url
results.append(res)
if res["ok"]:
print(f" [OK] avg={res['avg_ms']:.0f}ms (min={res['min_ms']:.0f}, max={res['max_ms']:.0f})")
else:
print(f" [FAIL] {res.get('error', res.get('status_code', '?'))}")
# 4. 生成报表
from datetime import datetime
date_str = datetime.now().strftime("%Y-%m-%d %H:%M")
date_file = datetime.now().strftime("%Y%m%d")
lines = [
"# 文章阅读与界面预览 GET 接口响应速度测试报告",
"",
f"**测试时间**: {date_str}",
f"**测试环境**: {ENV_LABEL} ({API_BASE})",
f"**每接口请求次数**: {ROUNDS}",
"",
"## 一、测试范围",
"",
"| 分类 | 接口 | 说明 |",
"|------|------|------|",
"| 界面预览 | GET /api/miniprogram/config | 配置(价格、功能开关等) |",
"| 界面预览 | GET /api/miniprogram/book/parts | 目录-篇章列表 |",
"| 界面预览 | GET /api/miniprogram/book/all-chapters | 全书章节列表 |",
"| 界面预览 | GET /api/miniprogram/book/chapters-by-part | 篇章内章节列表 |",
"| 文章阅读 | GET /api/miniprogram/book/chapter/:id | 按业务 id 获取章节内容 |",
"| 文章阅读 | GET /api/miniprogram/book/chapter/by-mid/:mid | 按 mid 获取章节内容 |",
"",
"## 二、响应速度结果",
"",
"| 接口 | 状态 | 平均(ms) | 最小(ms) | 最大(ms) |",
"|------|------|----------|----------|----------|",
]
for r in results:
status = "OK" if r["ok"] else "FAIL"
avg = f"{r['avg_ms']:.0f}" if r["ok"] else "-"
min_ms = f"{r['min_ms']:.0f}" if r["ok"] else "-"
max_ms = f"{r['max_ms']:.0f}" if r["ok"] else "-"
if not r["ok"]:
err = r.get("error", "") or f"HTTP {r.get('status_code', '?')}"
avg = err[:20] if err else "-"
lines.append(f"| {r['api']} | {status} | {avg} | {min_ms} | {max_ms} |")
# 汇总
ok_count = sum(1 for r in results if r["ok"])
total_count = len(results)
if ok_count == total_count:
avg_all = sum(r["avg_ms"] for r in results) / total_count
lines.extend([
"",
"## 三、汇总",
"",
f"- 通过: {ok_count}/{total_count}",
f"- 全部接口平均响应: {avg_all:.0f}ms",
"",
])
else:
lines.extend([
"",
"## 三、汇总",
"",
f"- 通过: {ok_count}/{total_count}",
f"- 失败: {total_count - ok_count} 个接口",
"",
])
report_content = "\n".join(lines)
# 5. 输出到控制台
print("\n" + "=" * 60)
print(report_content)
print("=" * 60)
# 6. 写入文件(项目根/开发文档)
report_dir = Path(__file__).resolve().parent.parent.parent.parent / "开发文档"
report_dir.mkdir(parents=True, exist_ok=True)
report_path = report_dir / f"测试报告-文章阅读与界面预览响应速度-{date_file}.md"
report_path.write_text(report_content, encoding="utf-8")
print(f"\n报表已保存: {report_path}")
return 0 if ok_count == total_count else 1
if __name__ == "__main__":
sys.exit(main())