Files
live-hub-py/services/yyb-worker/runtime/scripts/jsdom-pay.py
T
2026-08-12 23:20:43 +08:00

596 lines
27 KiB
Python

#!/usr/bin/env python3
"""使用 jsdom DeviceFP 完成一笔已创建 YYB 订单的 web_save。
本脚本不创建 mall 订单;先由 ``main.py mall auto`` 创建订单,再执行本脚本。
付款码只在服务端 ``web_save`` 返回 ``ret=0`` 后渲染。付款后通过商城官方订单
列表确认本次新出现的完成订单;该检查不触发付款或确认操作。
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import random
import re
import string
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from pyvm.algorithm import build_plaintext, derive_key1_from_key16, generate_encrypt_msg_offline # noqa: E402
from pyvm.login_profile import midas_login_params # noqa: E402
from pyvm.payment_errors import describe_payment_failure # noqa: E402
APPID = "1450243039"
GOODS_URL = "https://pay.qq.com/midas/minipay_v2/views/cpay/goods.shtml"
SAVE_URL = f"https://api.unipay.qq.com/v1/r/{APPID}/web_save"
PAGE_INFO_URL = f"https://api.unipay.qq.com/v1/r/{APPID}/web_page_info"
FP_URL = "https://api.unipay.qq.com/cgi-bin/fp-behv.fcg"
def load_json(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def parse_url_params(mall_response: dict) -> dict[str, str]:
try:
call_reply = json.loads(mall_response["data"]["call_reply"])
url_params = call_reply["data"]["url_params"]
except (KeyError, TypeError, json.JSONDecodeError) as exc:
raise ValueError("mall 响应缺少 data.call_reply.data.url_params") from exc
return {key: values[-1] for key, values in urllib.parse.parse_qs(
urllib.parse.urlparse(url_params).query, keep_blank_values=True).items()}
def cookie_header(cookies: dict[str, str]) -> str:
pairs = dict(cookies)
if not pairs.get("midas_openid") and pairs.get("openid"):
pairs["midas_openid"] = pairs["openid"]
if not pairs.get("midas_openkey") and pairs.get("accesstoken"):
pairs["midas_openkey"] = pairs["accesstoken"]
return "; ".join(f"{name}={value}" for name, value in pairs.items() if value)
def request_bytes(url: str, cookies: dict[str, str], body: bytes | None = None) -> str:
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
"Cookie": cookie_header(cookies),
"Referer": "https://pay.qq.com/",
}
if body is not None:
headers["Content-Type"] = "application/x-www-form-urlencoded"
headers["Origin"] = "https://pay.qq.com"
request = urllib.request.Request(url, data=body, headers=headers,
method="POST" if body is not None else "GET")
try:
with urllib.request.urlopen(request, timeout=30) as response:
return response.read().decode("utf-8", "replace")
except urllib.error.HTTPError as exc:
return exc.read().decode("utf-8", "replace")
def goods_page_url(cookies: dict[str, str], order: dict[str, str], zone_id: str = "1", pf: str = "") -> str:
openid = cookies.get("openid") or cookies.get("midas_openid")
openkey = cookies.get("accesstoken") or cookies.get("midas_openkey")
if not openid or not openkey:
raise ValueError("会话缺少 openid/accesstoken,请先完成授权扫码登录")
login = midas_login_params(cookies)
params = {
"appid": APPID,
"openid": openid,
"openkey": openkey,
"session_id": login["session_id"],
"session_type": login["session_type"],
"sandbox": "",
"wxappid": login["wx_appid"],
"qqAppid": login["qq_appid"],
"pf": pf or "mds_myappjp-__mds_myappjp_PC_aW9zd2hpdGVsaX0-android",
"buy_quantity": "1",
"goodstokenurl": order["url_params"],
"zoneid": zone_id,
"supportCloseConfirm": "1",
"t": str(int(time.time() * 1000)),
}
return GOODS_URL + "?" + urllib.parse.urlencode(params)
def extract_goods_state(html: str) -> tuple[list[int], str, str]:
ops = re.search(r"var\s+xMidasOps\s*=\s*\[([^]]+)]", html)
token = re.search(r'id="xMidasToken"\s+value="([0-9A-Fa-f]+)"', html)
anti = re.search(r'id="antiAutoScriptToken"\s+value="([0-9A-Fa-f]+)"', html)
if not ops or not token or not anti:
raise ValueError("goods 页面缺少 xMidasOps/xMidasToken/antiAutoScriptToken,登录态或订单已失效")
xmidas = [int(value) for value in ops.group(1).split(",") if value]
if len(xmidas) != 59640:
raise ValueError(f"goods xMidasOps 长度异常: {len(xmidas)}")
return xmidas, token.group(1).upper(), anti.group(1).upper()
def load_template_args() -> list:
runtime_template = ROOT / "replay/live/default"
runtime_args = runtime_template / "args-template.json"
if runtime_args.exists():
return load_json(runtime_args)
candidates = sorted((ROOT / "replay/live").glob("*/args-template.json"), reverse=True)
for path in candidates:
if path.exists():
return load_json(path)
raise FileNotFoundError("缺少归档 goods args-template.json")
def build_save_fields(order: dict[str, str], cookies: dict[str, str], web_token: str,
anti_token: str, encrypt_msg: str, zone_id: str, pf: str,
amount_fen: int) -> dict[str, str]:
"""按当前订单构造完整 web_save 表单,不能使用脱敏空模板。"""
if amount_fen <= 0:
raise ValueError("充值金额必须为正数")
login = midas_login_params(cookies)
fields = {
"token_id": order.get("token_id", ""),
"transaction_id": order.get("transaction_id", ""),
"out_trade_no": order.get("out_trade_no", ""),
"offer_type": order.get("offer_type", "0"),
"type": "bg",
"pf": pf,
"pfkey": "pfkey",
"from_h5": "1",
"pc_st": str(uuid.uuid4()).upper() + str(int(time.time() * 1000)),
"r": str(random.random()),
"openid": cookies.get("openid", ""),
"openkey": cookies.get("accesstoken", ""),
"session_id": login["session_id"],
"session_type": login["session_type"],
"sck": hashlib.md5((APPID + cookies.get("accesstoken", "")).encode()).hexdigest().upper(),
"anti_auto_script_token_id": anti_token,
"zoneid": zone_id,
"buy_quantity": "1",
"provide_uin": "",
"wx_appid": login["wx_appid"],
"extend": "",
"jump_url": "https://pay.qq.com/midas/minipay_v2/views/public/mb.shtml",
"wcp": f"type=CNY&amt={amount_fen}",
"biz_appid": "wx951bdcac522929b6",
"wx_direct_pay": "1",
"wx_publice_pay": "1",
"uuid": str(uuid.uuid4()).upper(),
"pay_method": "wechat",
"pushtype": "NodeJS",
"wx_order_interface": "1",
"encrypt_msg": encrypt_msg,
"base_key_version": "web_1.0.6",
"encrypt_way": "web_new_encrypt",
"web_token": web_token,
"webversion": "minipayv2",
"from_https": "1",
"t": str(int(time.time() * 1000)),
"__refer": "https://z.iwan.yyb.qq.com/",
}
# QQ 网页端会携带该字段;微信 OAuth 不携带,不能使用固定 appid 混入两条链路。
if login["qq_appid"]:
fields["offerid_for_qq_appid"] = login["qq_appid"]
return fields
def build_save_body(order: dict[str, str], cookies: dict[str, str], web_token: str,
anti_token: str, encrypt_msg: str, zone_id: str, pf: str,
amount_fen: int) -> str:
"""编码完整支付表单,确保加密明文和实际请求使用同一上下文。"""
return urllib.parse.urlencode(build_save_fields(
order, cookies, web_token, anti_token, encrypt_msg, zone_id, pf, amount_fen,
))
def build_page_info_body(order: dict[str, str], cookies: dict[str, str], anti_token: str,
zone_id: str, pf: str) -> str:
"""构造网页端 fp-behv 后的 web_page_info 表单。"""
login = midas_login_params(cookies)
fields = {
"token_id": order.get("token_id", ""),
"transaction_id": order.get("transaction_id", ""),
"out_trade_no": order.get("out_trade_no", ""),
"offer_type": order.get("offer_type", "0"),
"type": "bg",
"pf": pf,
"pfkey": "pfkey",
"from_h5": "1",
"pc_st": str(uuid.uuid4()).upper() + str(int(time.time() * 1000)),
"r": str(random.random()),
"openid": cookies.get("openid", ""),
"openkey": cookies.get("accesstoken", ""),
"session_id": login["session_id"],
"session_type": login["session_type"],
"sck": hashlib.md5((APPID + cookies.get("accesstoken", "")).encode()).hexdigest().upper(),
"anti_auto_script_token_id": anti_token,
"isusempaymode": "1",
"zoneid": zone_id,
"webversion": "minipayv2",
"from_https": "1",
"t": str(int(time.time() * 1000)),
"__refer": "https://z.iwan.yyb.qq.com/",
}
if login["qq_appid"]:
fields["offerid_for_qq_appid"] = login["qq_appid"]
return urllib.parse.urlencode(fields)
def make_encrypt_rand(params: dict[str, str], fk_extend: str, ts: str,
is_qq_login: bool) -> str:
"""按登录渠道生成页面已验证形态的 _rand,不能仅按长度替换控制字节。"""
prefix = "".join(random.choices(string.ascii_letters + string.digits, k=8))
# 微信历史成功请求固定使用 8 个随机字符加 \x01。_rand 位于加密明文中,
# 末控制字节是协议内容,不能为了对齐擅自替换为 QQ 使用的 \x03。
if not is_qq_login:
return prefix + "\x01"
# QQ 当前订单的明文长度会随 OAuth 字段变化;补齐后才能避免 VM 丢弃尾块。
base_length = len(build_plaintext(params, fk_extend, ts, "").encode("latin-1"))
padding_length = (-(base_length + len(prefix))) % 16
return prefix + ("\x03" * padding_length)
def save_web_save_request_meta(out_dir: Path, fields: dict[str, str], device_fp_length: int,
plaintext_length: int) -> None:
"""保存可比对的脱敏请求形态,避免在证据目录落盘支付凭据。"""
sensitive = ("token_id", "transaction_id", "out_trade_no", "openid", "openkey",
"sck", "anti_auto_script_token_id", "web_token", "encrypt_msg")
fingerprints = {
name: {
"length": len(fields.get(name, "")),
"sha256_prefix": hashlib.sha256(fields.get(name, "").encode()).hexdigest()[:12],
}
for name in sensitive
}
meta = {
"field_names": sorted(fields),
"plaintext_length": plaintext_length,
"encrypt_msg_length": len(fields.get("encrypt_msg", "")),
"device_fp_length": device_fp_length,
"sensitive_field_fingerprints": fingerprints,
}
(out_dir / "web-save-request-meta.json").write_text(
json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
)
def make_qr(sign: str, output: Path) -> None:
try:
import segno
except ImportError as exc:
raise RuntimeError("缺少 segno;请安装后重新执行: python3 -m pip install segno") from exc
output.parent.mkdir(parents=True, exist_ok=True)
segno.make(sign).save(str(output), scale=6, border=2)
def node_environment() -> dict[str, str]:
"""Prevent a developer's Node inspector setting from affecting the jsdom worker."""
environment = os.environ.copy()
environment.pop("NODE_OPTIONS", None)
return environment
def save_payment_status(path: Path, document: dict, baseline: dict[str, bool], matched: dict | None = None) -> None:
"""Persist a small, non-payment-side-effect status record for this run."""
from pyvm.order_status import completion_summary, order_ids
record = {
"checked_at": int(time.time()),
"baseline_order_states": baseline,
"listed_order_ids": sorted(order_ids(document)),
"matched_completion": completion_summary(matched) if matched else None,
}
path.write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def save_payment_meta(path: Path, order: dict[str, str], baseline: dict[str, bool],
document: dict, portal_serial_no: str = "") -> None:
"""Persist the order identifiers and pre-payment baseline needed by a later check.
``check`` only re-reads this file and never touches web_save, so re-running a
check is side-effect free.
The official order list (GetPrivateDomainOrderList) exposes its own ``order_id``
which equals web_save's ``portal_serial_no``; save it under ``order_id`` so the
completion check can match precisely instead of guessing by "newly completed".
"""
from pyvm.order_status import order_ids
record = {
"order_identifiers": {
"token_id": order.get("token_id", ""),
"transaction_id": order.get("transaction_id", ""),
"out_trade_no": order.get("out_trade_no", ""),
"order_id": portal_serial_no,
},
"baseline_order_states": baseline,
"baseline_order_ids": sorted(order_ids(document)),
"qr_created_at": int(time.time()),
}
path.write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def _match_finished_by_identifiers(listed: list[dict], identifiers: dict[str, str]) -> dict | None:
"""Match a finished order by the identifiers recorded at QR creation time.
The official order list may not carry token_id/out_trade_no, so no fallback
to "any newly completed order" is allowed: an unrelated order finishing on
the same account must not be reported as this payment.
"""
from pyvm.order_status import is_finished
for item in listed:
if is_finished(item) and any(
identifiers.get(key) and str(item.get(key, "")) == str(identifiers[key])
for key in identifiers
):
return item
return None
def cmd_check_only(session_path: Path, out_dir: Path) -> int:
"""Read-only payment completion check bound to a previously created QR.
Only an order identifier recorded at QR creation time counts as a match;
a fallback to "any newly completed order" is intentionally NOT used because
the order list cannot be proven to carry our token/out_trade_no fields,
and another order completing on the same account could be misjudged.
Exit codes: 0 = confirmed, 1 = not confirmed yet, 2 = check failed.
"""
from pyvm.order_status import (
completion_summary,
get_official_orders,
is_finished,
)
session = load_json(session_path)
cookies = dict(session.get("cookies", {}))
if not cookies:
raise ValueError("会话 cookies 为空,无法检测到账")
meta = load_json(out_dir / "payment-meta.json")
baseline = meta.get("baseline_order_states", {})
print("[jsdom-pay:check] 拉取官方订单列表...")
document = get_official_orders(cookies)
listed = [item for item in document.get("list", []) if isinstance(item, dict)]
matched = _match_finished_by_identifiers(listed, meta.get("order_identifiers", {}))
status_path = out_dir / "payment-status.json"
save_payment_status(status_path, document, baseline, matched)
if matched:
print(f"[jsdom-pay:check] 已按订单标识确认到账: {completion_summary(matched)}")
return 0
print("[jsdom-pay:check] 未匹配到本次订单标识,保持未确认状态,请人工核对")
return 1
def main() -> int:
parser = argparse.ArgumentParser(description="YYB: jsdom DeviceFP + web_save -> 微信付款码")
parser.add_argument("--session", default=str(ROOT / "config/mall-session.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("--qr", default=None, help="付款二维码 PNG 路径")
parser.add_argument("--wait", type=int, default=5, help="jsdom 等待 DeviceFP 的秒数")
parser.add_argument("--zone-id", default="1", help="所选游戏区服 ID")
parser.add_argument("--pf", default="", help="所选 Android/iOS 支付平台标识")
parser.add_argument("--amount-fen", type=int, default=0, help="所选点券的价格,单位分(check-only 模式不需要)")
parser.add_argument("--dry-run", action="store_true", help="仅拉取页面并生成 DeviceFP,不上报或创建付款码")
parser.add_argument("--payment-timeout", type=float, default=300,
help="付款码生成后等待订单完成的最长秒数")
parser.add_argument("--payment-interval", type=float, default=3,
help="订单完成状态检查间隔秒数")
parser.add_argument("--skip-payment-check", action="store_true", help="仅生成付款码,不等待订单完成")
parser.add_argument("--check-only", action="store_true",
help="只读检测到账(依赖 --out-dir 下已保存的 payment-meta.json),不创建订单")
args = parser.parse_args()
if args.payment_timeout <= 0 or args.payment_interval <= 0:
raise ValueError("--payment-timeout 和 --payment-interval 必须为正数")
session_path = Path(args.session).resolve()
response_path = Path(args.mall_response).resolve()
stamp = time.strftime("%Y%m%d-%H%M%S")
out_dir = Path(args.out_dir) if args.out_dir else ROOT / "config" / f"jsdom-order-{stamp}"
if args.check_only:
out_dir.mkdir(parents=True, exist_ok=True)
return cmd_check_only(session_path, out_dir)
if args.amount_fen <= 0:
raise ValueError("充值金额必须为正数")
payment_pf = args.pf or "mds_myappjp-__mds_myappjp_PC_aW9zd2hpdGVsaX0-android"
session = load_json(session_path)
cookies = dict(session.get("cookies", {}))
if not cookies:
raise ValueError("会话 cookies 为空,请先完成授权扫码登录")
response = load_json(response_path)
order = parse_url_params(response)
if not order.get("token_id"):
raise ValueError("mall 响应未包含 token_id")
try:
call_reply = json.loads(response["data"]["call_reply"])
order["url_params"] = call_reply["data"]["url_params"]
except (KeyError, TypeError, json.JSONDecodeError) as exc:
raise ValueError("mall 响应无法解析 url_params") from exc
stamp = time.strftime("%Y%m%d-%H%M%S")
out_dir = Path(args.out_dir) if args.out_dir else ROOT / "config" / f"jsdom-order-{stamp}"
out_dir.mkdir(parents=True, exist_ok=True)
url = goods_page_url(cookies, order, args.zone_id, args.pf)
print(f"[jsdom-pay] 订单: {order['token_id'][:20]}...")
print("[jsdom-pay] 拉取同订单 goods 页面...")
html = request_bytes(url, cookies)
html_path = out_dir / "goods.html"
html_path.write_text(html, encoding="utf-8")
xmidas, web_token, anti_token = extract_goods_state(html)
print(f"[jsdom-pay] goods: xMidasOps={len(xmidas)} web_token={web_token[:12]}...")
fp_path = out_dir / "device-fp.json"
command = [
"node", str(ROOT / "scripts/generate-devicefp-jsdom.mjs"),
"--html", str(html_path), "--goods-url", url, "--cookies", str(session_path),
"--output", str(fp_path), "--wait", str(args.wait * 1000),
]
print("[jsdom-pay] 运行 jsdom DeviceFP...")
subprocess.run(command, check=True, cwd=ROOT, env=node_environment())
fp = load_json(fp_path)
if args.dry_run:
print(f"[jsdom-pay] dry-run 完成,证据目录: {out_dir}")
return 0
print("[jsdom-pay] 上报 fp-behv...")
fp_response = request_bytes(fp.get("fp_url", FP_URL), cookies, fp["fp_body"].encode())
(out_dir / "fp-response.json").write_text(fp_response, encoding="utf-8")
try:
fp_json = json.loads(fp_response)
except json.JSONDecodeError:
fp_json = {}
if fp_json.get("ret") != 0:
print(f"[jsdom-pay] fp-behv 失败: ret={fp_json.get('ret')} {fp_json.get('msg', '')}")
return 1
# QQ 成功 HAR 包含该前置;微信历史成功链路没有,不能将 QQ 状态机混入微信请求。
if midas_login_params(cookies)["qq_appid"]:
print("[jsdom-pay] 拉取 QQ web_page_info...")
page_info = request_bytes(PAGE_INFO_URL, cookies, build_page_info_body(
order, cookies, anti_token, args.zone_id, payment_pf,
).encode())
(out_dir / "web-page-info-response.json").write_text(page_info, encoding="utf-8")
try:
page_info_json = json.loads(page_info)
except json.JSONDecodeError:
page_info_json = {}
if page_info_json.get("ret") != 0:
print(f"[jsdom-pay] QQ web_page_info 失败: ret={page_info_json.get('ret')} "
f"{page_info_json.get('msg', '')}")
print(f"[jsdom-pay] 证据目录: {out_dir}")
return 1
web_args = load_template_args()
# key 派生已破解(F-2052,2026-08-12):key16 可随机生成,key1 由反解器
# derive_key1_from_key16 求解(key16=Sbox[Te 链(key1)]),满足服务端派生校验,
# 无需浏览器捕获 key(微信渠道纯 HTTP 已 ret:0)。
key16 = [random.randrange(256) for _ in range(16)]
tables = [web_args[index][0] for index in (1, 2, 3, 4)]
key1 = derive_key1_from_key16(key16, te_tables=tables, sbox=web_args[5][0])
params = {
"token_id": order.get("token_id", ""),
"openid": cookies.get("openid", ""),
"openkey": cookies.get("accesstoken", ""),
"session_id": midas_login_params(cookies)["session_id"],
"session_type": midas_login_params(cookies)["session_type"],
"zoneid": args.zone_id,
"pay_method": "wechat",
"buy_quantity": "1",
"mb_pwd": "",
"pay_id": "",
"auth_key": "",
"card_value": "",
"accounttype": "",
"provide_uin": "",
"extend": "",
"from_h5": "1",
"webversion": "minipayv2",
}
params.update(midas_login_params(cookies))
now_seconds = str(int(time.time()))
fk_extend = "tdrc_session%3D" + fp["session_id"]
random_suffix = make_encrypt_rand(
params, fk_extend, now_seconds, bool(midas_login_params(cookies)["qq_appid"]),
)
plaintext_length = len(build_plaintext(params, fk_extend, now_seconds, random_suffix).encode("latin-1"))
encrypt_msg = generate_encrypt_msg_offline(
params, fk_extend, now_seconds, random_suffix,
key16=key16, key1=key1, args_template=web_args, xmidas=xmidas, xmidas_token=web_token,
)
fields = build_save_fields(
order, cookies, web_token, anti_token, encrypt_msg,
args.zone_id, payment_pf, args.amount_fen,
)
save_web_save_request_meta(out_dir, fields, int(fp.get("device_fp_length", 0)), plaintext_length)
body = urllib.parse.urlencode(fields)
from pyvm.order_status import order_completion_states, get_official_orders
# 尽量在发起支付前建立基线,缩小极快支付造成的检测窗口;失败不阻塞支付,付款码生成后重试。
baseline_document = None
baseline_states = None
try:
baseline_document = get_official_orders(cookies)
baseline_states = order_completion_states(baseline_document)
print("[jsdom-pay] 支付前订单基线已建立")
except Exception as exc: # noqa: BLE001
print(f"[jsdom-pay] 支付前建立基线失败({exc}),将在付款码生成后重试")
print("[jsdom-pay] 提交 web_save...")
raw = request_bytes(SAVE_URL, cookies, body.encode())
(out_dir / "web-save-response.json").write_text(raw, encoding="utf-8")
try:
response_json = json.loads(raw)
except json.JSONDecodeError:
response_json = {}
if response_json.get("ret") != 0:
detail = f"ret={response_json.get('ret')} err_code={response_json.get('err_code', '')} " \
f"{response_json.get('msg', '')}"
print(f"[jsdom-pay] web_save 失败: {detail}")
print(f"[jsdom-pay] {describe_payment_failure(cookies.get('logintype', ''), 'payment', detail)}")
print(f"[jsdom-pay] 证据目录: {out_dir}")
return 1
sign = response_json.get("info", {}).get("channel_info", {}).get("sign", "")
if not sign.startswith("weixin://"):
print("[jsdom-pay] web_save 成功,但响应没有微信付款链接")
return 1
info = response_json.get("info", {}) or {}
portal_serial_no = str(info.get("portal_serial_no", "") or "")
qr_path = Path(args.qr) if args.qr else out_dir / "wechat-pay.png"
make_qr(sign, qr_path)
print("[jsdom-pay] 微信付款码已生成")
print(f"[jsdom-pay] PNG: {qr_path}")
if baseline_document is None:
try:
baseline_document = get_official_orders(cookies)
baseline_states = order_completion_states(baseline_document)
except Exception as exc: # noqa: BLE001
# 付款码已生成,基线缺失只影响确认精度(仅按订单标识确认),不能阻断付款。
print(f"[jsdom-pay] 基线获取失败: {exc};将仅按订单标识确认到账")
baseline_document = {"list": []}
baseline_states = {}
print("[jsdom-pay] 记录付款前订单基线与本次订单标识...")
save_payment_meta(out_dir / "payment-meta.json", order, baseline_states, baseline_document, portal_serial_no)
if args.skip_payment_check:
print("[jsdom-pay] 已保存基线;后台将单独执行只读到账检测")
return 0
print("[jsdom-pay] 记录付款前订单列表,等待微信付款完成...")
status_path = out_dir / "payment-status.json"
save_payment_status(status_path, baseline_document, baseline_states)
identifiers = {key: value for key, value in {
"token_id": order.get("token_id", ""),
"transaction_id": order.get("transaction_id", ""),
"out_trade_no": order.get("out_trade_no", ""),
}.items() if value}
deadline = time.monotonic() + args.payment_timeout
while time.monotonic() < deadline:
time.sleep(args.payment_interval)
document = get_official_orders(cookies)
completed = _match_finished_by_identifiers(
[item for item in document.get("list", []) if isinstance(item, dict)], identifiers)
save_payment_status(status_path, document, baseline_states, completed)
if completed:
print("[jsdom-pay] 微信付款成功,商城订单已完成。")
print(f"[jsdom-pay] 状态记录: {status_path}")
return 0
print("[jsdom-pay] 在等待期限内未确认到本次订单完成。")
print(f"[jsdom-pay] 状态记录: {status_path}")
return 1
if __name__ == "__main__":
try:
raise SystemExit(main())
except (FileNotFoundError, ValueError, RuntimeError, subprocess.CalledProcessError) as exc:
print(f"错误: {exc}", file=sys.stderr)
raise SystemExit(2) from exc