#!/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 re import secrets 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 from pyvm.protocol import ( # noqa: E402 DEFAULT_ORDER_PF, GOODS_USER_AGENT, PAY_APPID, PAY_FP_URL, PAY_GOODS_URL, PAY_PAGE_INFO_URL, PAY_SAVE_URL, PAY_WEB_VERSION, PAY_WEBVERSION, goods_material_diagnostics, validate_goods_materials, ) APPID = PAY_APPID GOODS_URL = PAY_GOODS_URL SAVE_URL = PAY_SAVE_URL PAGE_INFO_URL = PAY_PAGE_INFO_URL FP_URL = PAY_FP_URL def make_private_directory(path: Path) -> None: """创建私有证据目录;已有目录也收紧权限。""" path.mkdir(parents=True, exist_ok=True) os.chmod(path, 0o700) def write_private_text(path: Path, content: str) -> None: """写入含订单或会话材料的原始证据,权限固定为 0600。""" path.write_text(content, encoding="utf-8") os.chmod(path, 0o600) def write_private_json(path: Path, value: dict) -> None: write_private_text(path, json.dumps(value, ensure_ascii=False, indent=2) + "\n") 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": GOODS_USER_AGENT, "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(secrets.SystemRandom().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": PAY_WEB_VERSION, "encrypt_way": "web_new_encrypt", "web_token": web_token, "webversion": PAY_WEBVERSION, "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(secrets.SystemRandom().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": PAY_WEBVERSION, "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(secrets.choice(string.ascii_letters + string.digits) for _ in range(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, } write_private_json(out_dir / "web-save-request-meta.json", meta) 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 make_private_directory(output.parent) segno.make(sign).save(str(output), scale=6, border=2) os.chmod(output, 0o600) 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, } write_private_json(path, record) 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()), } write_private_json(path, record) 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-") 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: make_private_directory(out_dir) return cmd_check_only(session_path, out_dir) if args.amount_fen <= 0: raise ValueError("充值金额必须为正数") payment_pf = args.pf or DEFAULT_ORDER_PF 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}" make_private_directory(out_dir) 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" write_private_text(html_path, html) 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()) write_private_text(out_dir / "fp-response.json", fp_response) 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()) write_private_text(out_dir / "web-page-info-response.json", page_info) 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() validate_goods_materials(web_args, xmidas) write_private_json(out_dir / "protocol-materials.json", goods_material_diagnostics(ROOT, web_args, xmidas)) print("[jsdom-pay] 协议材料校验通过") # key 派生已破解(F-2052,2026-08-12):key16 可随机生成,key1 由反解器 # derive_key1_from_key16 求解(key16=Sbox[Te 链(key1)]),满足服务端派生校验, # 无需浏览器捕获 key(微信渠道纯 HTTP 已 ret:0)。 key16 = [secrets.randbelow(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": PAY_WEBVERSION, } 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()) write_private_text(out_dir / "web-save-response.json", raw) 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