qq 也可以获取支付二维码 成功微信支付了
This commit is contained in:
@@ -26,12 +26,14 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from pyvm.algorithm import 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
|
||||
|
||||
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"))
|
||||
@@ -131,7 +133,7 @@ def build_save_fields(order: dict[str, str], cookies: dict[str, str], web_token:
|
||||
if amount_fen <= 0:
|
||||
raise ValueError("充值金额必须为正数")
|
||||
login = midas_login_params(cookies)
|
||||
return {
|
||||
fields = {
|
||||
"token_id": order.get("token_id", ""),
|
||||
"transaction_id": order.get("transaction_id", ""),
|
||||
"out_trade_no": order.get("out_trade_no", ""),
|
||||
@@ -163,7 +165,6 @@ def build_save_fields(order: dict[str, str], cookies: dict[str, str], web_token:
|
||||
"pushtype": "NodeJS",
|
||||
"wx_order_interface": "1",
|
||||
"encrypt_msg": encrypt_msg,
|
||||
"qq_appid": login["qq_appid"],
|
||||
"base_key_version": "web_1.0.6",
|
||||
"encrypt_way": "web_new_encrypt",
|
||||
"web_token": web_token,
|
||||
@@ -172,6 +173,10 @@ def build_save_fields(order: dict[str, str], cookies: dict[str, str], web_token:
|
||||
"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,
|
||||
@@ -183,6 +188,71 @@ def build_save_body(order: dict[str, str], cookies: dict[str, str], web_token: s
|
||||
))
|
||||
|
||||
|
||||
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) -> str:
|
||||
"""生成与当前订单明文长度匹配的 _rand,确保 goods VM 不截断尾块。"""
|
||||
prefix = "".join(random.choices(string.ascii_letters + string.digits, k=8))
|
||||
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
|
||||
@@ -323,6 +393,7 @@ def main() -> int:
|
||||
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", {}))
|
||||
@@ -366,13 +437,36 @@ def main() -> int:
|
||||
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
|
||||
|
||||
print("[jsdom-pay] 拉取 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] 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()
|
||||
payment_pf = args.pf or "mds_myappjp-__mds_myappjp_PC_aW9zd2hpdGVsaX0-android"
|
||||
# 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])
|
||||
random_suffix = "".join(random.choices(string.ascii_letters + string.digits, k=8)) + "\x01"
|
||||
params = {
|
||||
"token_id": order.get("token_id", ""),
|
||||
"openid": cookies.get("openid", ""),
|
||||
@@ -394,14 +488,19 @@ def main() -> int:
|
||||
}
|
||||
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)
|
||||
plaintext_length = len(build_plaintext(params, fk_extend, now_seconds, random_suffix).encode("latin-1"))
|
||||
encrypt_msg = generate_encrypt_msg_offline(
|
||||
params, "tdrc_session%3D" + fp["session_id"], now_seconds, random_suffix,
|
||||
params, fk_extend, now_seconds, random_suffix,
|
||||
key16=key16, key1=key1, args_template=web_args, xmidas=xmidas, xmidas_token=web_token,
|
||||
)
|
||||
body = build_save_body(
|
||||
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
|
||||
|
||||
# 尽量在发起支付前建立基线,缩小极快支付造成的检测窗口;失败不阻塞支付,付款码生成后重试。
|
||||
@@ -421,7 +520,10 @@ def main() -> int:
|
||||
except json.JSONDecodeError:
|
||||
response_json = {}
|
||||
if response_json.get("ret") != 0:
|
||||
print(f"[jsdom-pay] web_save 失败: ret={response_json.get('ret')} {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] 证据目录: {out_dir}")
|
||||
return 1
|
||||
sign = response_json.get("info", {}).get("channel_info", {}).get("sign", "")
|
||||
|
||||
@@ -23,11 +23,21 @@ from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
DEFAULT_DATA = ROOT / "config" / "worker-jobs"
|
||||
WORKER_KEY = os.environ.get("YYB_WORKER_KEY", "")
|
||||
_jobs: dict[str, dict] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
from pyvm.payment_errors import describe_payment_failure # noqa: E402
|
||||
|
||||
|
||||
def _payment_failure_message(job: dict, phase: str, fallback: str) -> str:
|
||||
"""从阶段日志提取业务码,向管理端返回准确且可操作的失败信息。"""
|
||||
detail = "\n".join(job.get("logs", []))
|
||||
message = describe_payment_failure(str(job.get("provider", "")), phase, detail)
|
||||
return message if message else fallback
|
||||
|
||||
|
||||
def _load_selector():
|
||||
path = ROOT / "scripts" / "select-peace-elite.py"
|
||||
@@ -161,7 +171,7 @@ def _payment_stage(job_id: str, command: list[str], phase: str, running_message:
|
||||
return False
|
||||
job["status"] = "failed"
|
||||
job["phase"] = phase
|
||||
job["message"] = f"{failed_message}(退出码 {code})"
|
||||
job["message"] = _payment_failure_message(job, phase, failed_message)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user