style: 统一 Ruff 代码格式
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
付款码只在服务端 ``web_save`` 返回 ``ret=0`` 后渲染。付款后通过商城官方订单
|
||||
列表确认本次新出现的完成订单;该检查不触发付款或确认操作。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -26,7 +27,11 @@ 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.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
|
||||
@@ -64,6 +69,8 @@ def write_private_text(path: Path, content: str) -> None:
|
||||
|
||||
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"))
|
||||
|
||||
@@ -74,8 +81,12 @@ def parse_url_params(mall_response: dict) -> dict[str, str]:
|
||||
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()}
|
||||
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:
|
||||
@@ -96,8 +107,9 @@ def request_bytes(url: str, cookies: dict[str, str], body: bytes | None = None)
|
||||
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")
|
||||
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")
|
||||
@@ -105,7 +117,9 @@ def request_bytes(url: str, cookies: dict[str, str], body: bytes | None = None)
|
||||
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:
|
||||
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:
|
||||
@@ -135,7 +149,9 @@ def extract_goods_state(html: str) -> tuple[list[int], str, str]:
|
||||
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,登录态或订单已失效")
|
||||
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)}")
|
||||
@@ -147,16 +163,25 @@ def load_template_args() -> list:
|
||||
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)
|
||||
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]:
|
||||
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("充值金额必须为正数")
|
||||
@@ -176,7 +201,9 @@ def build_save_fields(order: dict[str, str], cookies: dict[str, str], web_token:
|
||||
"openkey": cookies.get("accesstoken", ""),
|
||||
"session_id": login["session_id"],
|
||||
"session_type": login["session_type"],
|
||||
"sck": hashlib.md5((APPID + cookies.get("accesstoken", "")).encode()).hexdigest().upper(),
|
||||
"sck": hashlib.md5((APPID + cookies.get("accesstoken", "")).encode())
|
||||
.hexdigest()
|
||||
.upper(),
|
||||
"anti_auto_script_token_id": anti_token,
|
||||
"zoneid": zone_id,
|
||||
"buy_quantity": "1",
|
||||
@@ -207,17 +234,38 @@ def build_save_fields(order: dict[str, str], cookies: dict[str, str], web_token:
|
||||
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:
|
||||
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,
|
||||
))
|
||||
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:
|
||||
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 = {
|
||||
@@ -235,7 +283,9 @@ def build_page_info_body(order: dict[str, str], cookies: dict[str, str], anti_to
|
||||
"openkey": cookies.get("accesstoken", ""),
|
||||
"session_id": login["session_id"],
|
||||
"session_type": login["session_type"],
|
||||
"sck": hashlib.md5((APPID + cookies.get("accesstoken", "")).encode()).hexdigest().upper(),
|
||||
"sck": hashlib.md5((APPID + cookies.get("accesstoken", "")).encode())
|
||||
.hexdigest()
|
||||
.upper(),
|
||||
"anti_auto_script_token_id": anti_token,
|
||||
"isusempaymode": "1",
|
||||
"zoneid": zone_id,
|
||||
@@ -249,10 +299,13 @@ def build_page_info_body(order: dict[str, str], cookies: dict[str, str], anti_to
|
||||
return urllib.parse.urlencode(fields)
|
||||
|
||||
|
||||
def make_encrypt_rand(params: dict[str, str], fk_extend: str, ts: str,
|
||||
is_qq_login: bool) -> str:
|
||||
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))
|
||||
prefix = "".join(
|
||||
secrets.choice(string.ascii_letters + string.digits) for _ in range(8)
|
||||
)
|
||||
# 微信历史成功请求固定使用 8 个随机字符加 \x01。_rand 位于加密明文中,
|
||||
# 末控制字节是协议内容,不能为了对齐擅自替换为 QQ 使用的 \x03。
|
||||
if not is_qq_login:
|
||||
@@ -264,15 +317,27 @@ def make_encrypt_rand(params: dict[str, str], fk_extend: str, ts: str,
|
||||
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:
|
||||
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")
|
||||
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],
|
||||
"sha256_prefix": hashlib.sha256(fields.get(name, "").encode()).hexdigest()[
|
||||
:12
|
||||
],
|
||||
}
|
||||
for name in sensitive
|
||||
}
|
||||
@@ -290,7 +355,9 @@ 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
|
||||
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)
|
||||
@@ -303,7 +370,9 @@ def node_environment() -> dict[str, str]:
|
||||
return environment
|
||||
|
||||
|
||||
def save_payment_status(path: Path, document: dict, baseline: dict[str, bool], matched: dict | None = None) -> None:
|
||||
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
|
||||
|
||||
@@ -316,8 +385,13 @@ def save_payment_status(path: Path, document: dict, baseline: dict[str, bool], m
|
||||
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:
|
||||
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
|
||||
@@ -343,7 +417,9 @@ def save_payment_meta(path: Path, order: dict[str, str], baseline: dict[str, boo
|
||||
write_private_json(path, record)
|
||||
|
||||
|
||||
def _match_finished_by_identifiers(listed: list[dict], identifiers: dict[str, str]) -> dict | None:
|
||||
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
|
||||
@@ -397,23 +473,52 @@ def cmd_check_only(session_path: Path, out_dir: Path) -> int:
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="YYB: jsdom DeviceFP + web_save -> 微信付款码")
|
||||
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(
|
||||
"--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(
|
||||
"--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),不创建订单")
|
||||
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 必须为正数")
|
||||
@@ -421,7 +526,9 @@ def main() -> int:
|
||||
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}"
|
||||
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)
|
||||
@@ -444,7 +551,9 @@ def main() -> int:
|
||||
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 = (
|
||||
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]}...")
|
||||
@@ -457,9 +566,18 @@ def main() -> int:
|
||||
|
||||
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),
|
||||
"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())
|
||||
@@ -469,36 +587,53 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
print("[jsdom-pay] 上报 fp-behv...")
|
||||
fp_response = request_bytes(fp.get("fp_url", FP_URL), cookies, fp["fp_body"].encode())
|
||||
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', '')}")
|
||||
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())
|
||||
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] 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))
|
||||
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)]),满足服务端派生校验,
|
||||
@@ -529,18 +664,38 @@ def main() -> int:
|
||||
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"]),
|
||||
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")
|
||||
)
|
||||
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,
|
||||
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,
|
||||
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
|
||||
)
|
||||
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
|
||||
|
||||
@@ -561,10 +716,14 @@ def main() -> int:
|
||||
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', '')}"
|
||||
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] {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", "")
|
||||
@@ -588,7 +747,13 @@ def main() -> int:
|
||||
baseline_document = {"list": []}
|
||||
baseline_states = {}
|
||||
print("[jsdom-pay] 记录付款前订单基线与本次订单标识...")
|
||||
save_payment_meta(out_dir / "payment-meta.json", order, baseline_states, baseline_document, portal_serial_no)
|
||||
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
|
||||
@@ -596,17 +761,23 @@ def main() -> int:
|
||||
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}
|
||||
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)
|
||||
[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] 微信付款成功,商城订单已完成。")
|
||||
@@ -620,6 +791,11 @@ def main() -> int:
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (FileNotFoundError, ValueError, RuntimeError, subprocess.CalledProcessError) as exc:
|
||||
except (
|
||||
FileNotFoundError,
|
||||
ValueError,
|
||||
RuntimeError,
|
||||
subprocess.CalledProcessError,
|
||||
) as exc:
|
||||
print(f"错误: {exc}", file=sys.stderr)
|
||||
raise SystemExit(2) from exc
|
||||
|
||||
Reference in New Issue
Block a user