优化支付耗时和日志

This commit is contained in:
yml2213
2026-08-12 23:20:43 +08:00
parent f1a22e87e9
commit 74a85c8328
5 changed files with 81 additions and 71 deletions
+8 -12
View File
@@ -18,7 +18,7 @@ BACKEND_HOST="${BACKEND_HOST:-0.0.0.0}"
BACKEND_PORT="${BACKEND_PORT:-8800}" BACKEND_PORT="${BACKEND_PORT:-8800}"
FRONTEND_HOST="${FRONTEND_HOST:-0.0.0.0}" FRONTEND_HOST="${FRONTEND_HOST:-0.0.0.0}"
FRONTEND_PORT="${FRONTEND_PORT:-5174}" FRONTEND_PORT="${FRONTEND_PORT:-5174}"
LOG_LEVEL="${LOG_LEVEL:-DEBUG}" LOG_LEVEL="${LOG_LEVEL:-INFO}"
MYSQL_IMAGE="${MYSQL_IMAGE:-mysql:8.4}" MYSQL_IMAGE="${MYSQL_IMAGE:-mysql:8.4}"
MYSQL_BIND_HOST="${MYSQL_BIND_HOST:-127.0.0.1}" MYSQL_BIND_HOST="${MYSQL_BIND_HOST:-127.0.0.1}"
MYSQL_HOST_PORT="${MYSQL_HOST_PORT:-3307}" MYSQL_HOST_PORT="${MYSQL_HOST_PORT:-3307}"
@@ -40,10 +40,7 @@ DEV_YYB_WORKER_URL="${DEV_YYB_WORKER_URL:-http://127.0.0.1:${YYB_WORKER_PORT}}"
YYB_WORKER_DATA_DIR="${YYB_WORKER_DATA_DIR:-$ROOT_DIR/data/yyb-worker-dev}" YYB_WORKER_DATA_DIR="${YYB_WORKER_DATA_DIR:-$ROOT_DIR/data/yyb-worker-dev}"
YYB_WORKER_NODE_MODULES="$ROOT_DIR/services/yyb-worker/node_modules" YYB_WORKER_NODE_MODULES="$ROOT_DIR/services/yyb-worker/node_modules"
LOG_DIR="$ROOT_DIR/logs" LOG_DIR="$ROOT_DIR/logs"
LOG_DATE="$(date +%Y-%m-%d)" APP_LOG="$LOG_DIR/app.log"
BACKEND_LOG="$LOG_DIR/backend-$LOG_DATE.log"
FRONTEND_LOG="$LOG_DIR/frontend-$LOG_DATE.log"
WORKER_LOG="$LOG_DIR/yyb-worker-$LOG_DATE.log"
BACKEND_PID="" BACKEND_PID=""
FRONTEND_PID="" FRONTEND_PID=""
@@ -161,7 +158,7 @@ start_yyb_worker() {
uv run --project "$ROOT_DIR" python scripts/yyb-worker.py \ uv run --project "$ROOT_DIR" python scripts/yyb-worker.py \
--host 127.0.0.1 \ --host 127.0.0.1 \
--port "$YYB_WORKER_PORT" \ --port "$YYB_WORKER_PORT" \
--data-dir "$YYB_WORKER_DATA_DIR" 2>&1 | tee -a "$WORKER_LOG" --data-dir "$YYB_WORKER_DATA_DIR"
) & ) &
WORKER_PID=$! WORKER_PID=$!
@@ -173,7 +170,7 @@ start_yyb_worker() {
fi fi
if [ "$i" = "15" ]; then if [ "$i" = "15" ]; then
echo "YYB Worker 启动超时,查看日志:" echo "YYB Worker 启动超时,查看日志:"
tail -n 100 "$WORKER_LOG" 2>/dev/null || true echo "请查看当前终端中的 Worker 输出。"
exit 1 exit 1
fi fi
sleep 1 sleep 1
@@ -211,9 +208,8 @@ echo " 前端: http://localhost:${FRONTEND_PORT}"
echo " API 代理: ${BACKEND_PROXY_TARGET}" echo " API 代理: ${BACKEND_PROXY_TARGET}"
echo " MySQL: ${DB_HOST}:${DB_PORT}/${DB_NAME}" echo " MySQL: ${DB_HOST}:${DB_PORT}/${DB_NAME}"
echo " YYB Worker: ${DEV_YYB_WORKER_URL}" echo " YYB Worker: ${DEV_YYB_WORKER_URL}"
echo " 后端日志: ${BACKEND_LOG}" echo " 应用日志: ${APP_LOG}"
echo " 前端日志: ${FRONTEND_LOG}" echo " 前端与 Worker 输出: 当前终端"
echo " Worker 日志: ${WORKER_LOG}"
echo " 退出: Ctrl+C" echo " 退出: Ctrl+C"
echo "==============================" echo "=============================="
echo "" echo ""
@@ -229,14 +225,14 @@ echo ""
--reload \ --reload \
--reload-dir core \ --reload-dir core \
--reload-dir utils \ --reload-dir utils \
--reload-dir web/backend 2>&1 | tee -a "$BACKEND_LOG" --reload-dir web/backend
) & ) &
BACKEND_PID=$! BACKEND_PID=$!
( (
cd web/frontend cd web/frontend
VITE_BACKEND_TARGET="$BACKEND_PROXY_TARGET" \ VITE_BACKEND_TARGET="$BACKEND_PROXY_TARGET" \
npm run dev -- --host "$FRONTEND_HOST" --port "$FRONTEND_PORT" 2>&1 | tee -a "$FRONTEND_LOG" npm run dev -- --host "$FRONTEND_HOST" --port "$FRONTEND_PORT"
) & ) &
FRONTEND_PID=$! FRONTEND_PID=$!
@@ -23,7 +23,7 @@ const htmlPath = getArg('--html');
const goodsUrl = getArg('--goods-url'); const goodsUrl = getArg('--goods-url');
const cookiePath = getArg('--cookies'); const cookiePath = getArg('--cookies');
const outputPath = getArg('--output'); const outputPath = getArg('--output');
const waitMs = Number.parseInt(getArg('--wait', '12000'), 10); const waitMs = Number.parseInt(getArg('--wait', '5000'), 10);
const useArchivedAssets = argv.includes('--archived-assets'); const useArchivedAssets = argv.includes('--archived-assets');
function fail(message) { function fail(message) {
@@ -61,8 +61,18 @@ class GoodsLoader extends ResourceLoader {
const session = cookiePath ? JSON.parse(fs.readFileSync(cookiePath, 'utf8')) : {}; const session = cookiePath ? JSON.parse(fs.readFileSync(cookiePath, 'utf8')) : {};
const cookies = session.cookies || {}; const cookies = session.cookies || {};
const captured = [];
const resourceErrors = []; const resourceErrors = [];
let capturedFp = null;
let resolveFpCapture = null;
const startedAt = Date.now();
function captureFp(url, body) {
const values = Object.fromEntries(new URLSearchParams(body));
if (!values.SessionID || !values.DeviceFP) return;
capturedFp = { url, body };
if (resolveFpCapture) resolveFpCapture();
}
const dom = new JSDOM(fs.readFileSync(htmlPath, 'utf8'), { const dom = new JSDOM(fs.readFileSync(htmlPath, 'utf8'), {
url: goodsUrl, url: goodsUrl,
referrer: 'https://z.iwan.yyb.qq.com/', referrer: 'https://z.iwan.yyb.qq.com/',
@@ -88,7 +98,7 @@ const dom = new JSDOM(fs.readFileSync(htmlPath, 'utf8'), {
send(body) { send(body) {
const requestBody = String(body || ''); const requestBody = String(body || '');
if (this.__yybUrl.includes('fp-behv.fcg')) { if (this.__yybUrl.includes('fp-behv.fcg')) {
captured.push({ url: this.__yybUrl, body: requestBody }); captureFp(this.__yybUrl, requestBody);
} }
this.readyState = 4; this.readyState = 4;
this.status = 200; this.status = 200;
@@ -103,18 +113,23 @@ const dom = new JSDOM(fs.readFileSync(htmlPath, 'utf8'), {
}, },
}); });
await new Promise(resolve => setTimeout(resolve, Number.isFinite(waitMs) ? waitMs : 12000)); const timeoutMs = Number.isFinite(waitMs) && waitMs > 0 ? waitMs : 5000;
const fp = captured.at(-1); let timeoutId;
if (!fp) { await Promise.race([
new Promise(resolve => {
resolveFpCapture = resolve;
if (capturedFp) resolve();
}),
new Promise(resolve => { timeoutId = setTimeout(resolve, timeoutMs); }),
]);
clearTimeout(timeoutId);
if (!capturedFp) {
dom.window.close(); dom.window.close();
fail(`未生成 fp-behv;加载错误: ${resourceErrors.slice(0, 3).join(' | ') || '无'}`); fail(`未生成 fp-behv;加载错误: ${resourceErrors.slice(0, 3).join(' | ') || '无'}`);
} }
const fp = capturedFp;
const values = Object.fromEntries(new URLSearchParams(fp.body)); const values = Object.fromEntries(new URLSearchParams(fp.body));
if (!values.SessionID || !values.DeviceFP) {
dom.window.close();
fail('fp-behv 缺少 SessionID 或 DeviceFP');
}
const result = { const result = {
generated_at: new Date().toISOString(), generated_at: new Date().toISOString(),
goods_url: goodsUrl, goods_url: goodsUrl,
@@ -130,3 +145,4 @@ dom.window.close();
console.log(`DeviceFP 已保存: ${outputPath}`); console.log(`DeviceFP 已保存: ${outputPath}`);
console.log(`SessionID: ${result.session_id}`); console.log(`SessionID: ${result.session_id}`);
console.log(`DeviceFP 长度: ${result.device_fp_length}`); console.log(`DeviceFP 长度: ${result.device_fp_length}`);
console.log(`DeviceFP 捕获耗时: ${Date.now() - startedAt}ms`);
@@ -375,7 +375,7 @@ def main() -> int:
parser.add_argument("--mall-response", default=str(ROOT / "config/mall-order-response.json")) parser.add_argument("--mall-response", default=str(ROOT / "config/mall-order-response.json"))
parser.add_argument("--out-dir", default=None, help="运行证据目录;默认 config/jsdom-order-<timestamp>") parser.add_argument("--out-dir", default=None, help="运行证据目录;默认 config/jsdom-order-<timestamp>")
parser.add_argument("--qr", default=None, help="付款二维码 PNG 路径") parser.add_argument("--qr", default=None, help="付款二维码 PNG 路径")
parser.add_argument("--wait", type=int, default=12, help="jsdom 等待 DeviceFP 的秒数") parser.add_argument("--wait", type=int, default=5, help="jsdom 等待 DeviceFP 的秒数")
parser.add_argument("--zone-id", default="1", help="所选游戏区服 ID") parser.add_argument("--zone-id", default="1", help="所选游戏区服 ID")
parser.add_argument("--pf", default="", help="所选 Android/iOS 支付平台标识") parser.add_argument("--pf", default="", help="所选 Android/iOS 支付平台标识")
parser.add_argument("--amount-fen", type=int, default=0, help="所选点券的价格,单位分(check-only 模式不需要)") parser.add_argument("--amount-fen", type=int, default=0, help="所选点券的价格,单位分(check-only 模式不需要)")
@@ -61,8 +61,9 @@ def _safe_log(job: dict, line: str) -> None:
clean = re.sub(r"(?:token|openid|openkey|cookie)=\S+", "[敏感字段已隐藏]", clean, flags=re.I) clean = re.sub(r"(?:token|openid|openkey|cookie)=\S+", "[敏感字段已隐藏]", clean, flags=re.I)
clean = re.sub(r"(?:pay_token|web_token|anti_token|session_id|sessionid)\s*[:= ]\s*\S+", clean = re.sub(r"(?:pay_token|web_token|anti_token|session_id|sessionid)\s*[:= ]\s*\S+",
"[敏感字段已隐藏]", clean, flags=re.I) "[敏感字段已隐藏]", clean, flags=re.I)
timestamp = time.strftime("%H:%M:%S")
with _lock: with _lock:
job["logs"] = (job.get("logs", []) + [clean.strip()])[-100:] job["logs"] = (job.get("logs", []) + [f"[{timestamp}] {clean.strip()}"])[-100:]
def _run_process(job_id: str, command: list[str], phase: str, mark_success: bool = True) -> int: def _run_process(job_id: str, command: list[str], phase: str, mark_success: bool = True) -> int:
@@ -79,6 +80,8 @@ def _run_process(job_id: str, command: list[str], phase: str, mark_success: bool
with _lock: with _lock:
job["phase"] = phase job["phase"] = phase
job["status"] = "running" job["status"] = "running"
stage_started = time.monotonic()
_safe_log(job, f"[{phase}] 开始执行")
try: try:
process = subprocess.Popen(command, cwd=ROOT, stdout=subprocess.PIPE, process = subprocess.Popen(command, cwd=ROOT, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, stderr=subprocess.STDOUT, text=True,
@@ -89,6 +92,7 @@ def _run_process(job_id: str, command: list[str], phase: str, mark_success: bool
for line in process.stdout: for line in process.stdout:
_safe_log(job, line) _safe_log(job, line)
code = process.wait() code = process.wait()
_safe_log(job, f"[{phase}] 执行结束,耗时 {time.monotonic() - stage_started:.1f}")
with _lock: with _lock:
job["process_pid"] = None job["process_pid"] = None
if code != 0: if code != 0:
@@ -109,6 +113,7 @@ def _run_process(job_id: str, command: list[str], phase: str, mark_success: bool
job["message"] = "付款流程已完成" job["message"] = "付款流程已完成"
return code return code
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
_safe_log(job, f"[{phase}] 执行异常,耗时 {time.monotonic() - stage_started:.1f}")
with _lock: with _lock:
job["status"] = "failed" job["status"] = "failed"
job["message"] = str(exc) job["message"] = str(exc)
@@ -189,10 +194,13 @@ def _check_payment_once(job_id: str) -> int:
"--out-dir", str(directory / "jsdom-order")] "--out-dir", str(directory / "jsdom-order")]
environment = os.environ.copy() environment = os.environ.copy()
environment.pop("NODE_OPTIONS", None) environment.pop("NODE_OPTIONS", None)
check_started = time.monotonic()
_safe_log(job, "[到账检测] 开始执行")
try: try:
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True,
env=environment, timeout=90) env=environment, timeout=90)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
_safe_log(job, f"[到账检测] 执行超时,耗时 {time.monotonic() - check_started:.1f}")
with _lock: with _lock:
job["payment_last_checked_at"] = int(time.time()) job["payment_last_checked_at"] = int(time.time())
return 2 return 2
@@ -200,6 +208,7 @@ def _check_payment_once(job_id: str) -> int:
_safe_log(job, line) _safe_log(job, line)
for line in (result.stderr or "").splitlines(): for line in (result.stderr or "").splitlines():
_safe_log(job, line) _safe_log(job, line)
_safe_log(job, f"[到账检测] 执行结束,耗时 {time.monotonic() - check_started:.1f}")
with _lock: with _lock:
job["payment_last_checked_at"] = int(time.time()) job["payment_last_checked_at"] = int(time.time())
return result.returncode return result.returncode
+36 -47
View File
@@ -1,33 +1,28 @@
"""日志配置模块""" """日志配置模块"""
import os
import sys import sys
import logging import logging
from datetime import date import re
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from pathlib import Path from pathlib import Path
from loguru import logger from loguru import logger
def _should_rotate(message, file): class _RelevantAccessFilter(logging.Filter):
"""自定义轮转条件:每天0点轮转,或单文件超过10M""" """仅保留异常 HTTP 请求,避免前端轮询淹没业务日志"""
if not file:
return False _STATUS_RE = re.compile(r'"\s+(\d{3})\b')
try:
# 单文件超过 10M 则轮转 def filter(self, record: logging.LogRecord) -> bool:
if os.path.getsize(file.name) > 10 * 1024 * 1024: if record.levelno >= logging.WARNING:
return True return True
# 跨天则轮转:日志日期 ≠ 文件最后修改日期 matched = self._STATUS_RE.search(record.getMessage())
file_date = date.fromtimestamp(os.path.getmtime(file.name)) return bool(matched and int(matched.group(1)) >= 400)
log_date = message.record["time"].date()
return file_date != log_date
except OSError:
return False
def _configure_standard_logging(level: str, log_dir: Path) -> None: def _configure_standard_logging(level: str, log_dir: Path) -> logging.Logger:
""" Uvicorn/FastAPI 的标准日志同步到控制台和文件。""" """业务日志和关键框架日志收敛到单一轮转文件。"""
log_level = getattr(logging, level.upper(), logging.INFO) log_level = getattr(logging, level.upper(), logging.INFO)
formatter = logging.Formatter( formatter = logging.Formatter(
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s" "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
@@ -38,17 +33,30 @@ def _configure_standard_logging(level: str, log_dir: Path) -> None:
console_handler.setFormatter(formatter) console_handler.setFormatter(formatter)
file_handler = TimedRotatingFileHandler( file_handler = TimedRotatingFileHandler(
log_dir / "server.log", when="midnight", backupCount=7, encoding="utf-8" log_dir / "app.log", when="midnight", backupCount=7, encoding="utf-8"
) )
file_handler.setLevel(log_level) file_handler.setLevel(log_level)
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
for name in ("uvicorn", "uvicorn.error", "uvicorn.access", "fastapi", "starlette"): app_logger = logging.getLogger("app")
app_logger.handlers.clear()
app_logger.handlers = [console_handler, file_handler]
app_logger.setLevel(log_level)
app_logger.propagate = False
for name in ("uvicorn", "uvicorn.error", "fastapi", "starlette"):
standard_logger = logging.getLogger(name) standard_logger = logging.getLogger(name)
standard_logger.handlers = [console_handler, file_handler] standard_logger.handlers = [console_handler, file_handler]
standard_logger.setLevel(log_level) standard_logger.setLevel(log_level)
standard_logger.propagate = False standard_logger.propagate = False
access_logger = logging.getLogger("uvicorn.access")
access_logger.handlers = [console_handler, file_handler]
access_logger.filters = [_RelevantAccessFilter()]
access_logger.setLevel(logging.INFO)
access_logger.propagate = False
return app_logger
def setup_logger(level: str = "INFO", log_dir: str = None, log_file: str = None) -> None: def setup_logger(level: str = "INFO", log_dir: str = None, log_file: str = None) -> None:
""" """
@@ -56,45 +64,26 @@ def setup_logger(level: str = "INFO", log_dir: str = None, log_file: str = None)
Args: Args:
level: 日志级别 level: 日志级别
log_dir: 日志目录路径(文件名按日期自动生成,如 app-2026-06-24.log log_dir: 日志目录路径(写入 app.log,按日轮转
log_file: 日志文件路径(兼容旧接口,优先级低于 log_dir) log_file: 日志文件路径(兼容旧接口,优先级低于 log_dir)
""" """
# 移除默认handler # 移除默认 handler,由标准 logging 统一处理控制台和文件输出。
logger.remove() logger.remove()
# 控制台输出
logger.add(
sys.stdout,
level=level,
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
"<level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
"<level>{message}</level>",
colorize=True,
)
# 确定日志文件路径 # 确定日志文件路径
if log_dir: if log_dir:
# 新接口:按目录 + 日期命名
dir_path = Path(log_dir) dir_path = Path(log_dir)
dir_path.mkdir(parents=True, exist_ok=True) dir_path.mkdir(parents=True, exist_ok=True)
_configure_standard_logging(level, dir_path) app_logger = _configure_standard_logging(level, dir_path)
file_path = str(dir_path / "app-{time:YYYY-MM-DD}.log")
elif log_file: elif log_file:
# 兼容旧接口
file_path = Path(log_file) file_path = Path(log_file)
file_path.parent.mkdir(parents=True, exist_ok=True) file_path.parent.mkdir(parents=True, exist_ok=True)
file_path = str(file_path) app_logger = _configure_standard_logging(level, file_path.parent)
else: else:
return return
# 文件输出:按天命名 + 单文件最大10M轮转 + 保留7天 def forward_to_standard(message) -> None:
logger.add( record = message.record
file_path, app_logger.log(record["level"].no, f"{record['name']} | {record['message']}")
level=level,
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}", logger.add(forward_to_standard, level=level, format="{message}")
rotation=_should_rotate, # 超过10M轮转
retention="7 days", # 保留7天
compression="zip", # 旧日志自动压缩节省空间
encoding="utf-8",
)