qq 也可以获取支付二维码 成功微信支付了

This commit is contained in:
yml2213
2026-08-12 21:41:26 +08:00
parent 4c978b923b
commit d610933f87
7 changed files with 286 additions and 18 deletions
+6 -3
View File
@@ -36,10 +36,12 @@ from pyvm.algorithm import build_plaintext, generate_encrypt_msg, generate_encry
from pyvm.mall import MallSession, generate_encrypt_msg as mall_generate # noqa: E402
from pyvm.session import SessionState, load_session # noqa: E402
from pyvm.login_profile import midas_login_params # noqa: E402
from pyvm.payment_errors import describe_payment_failure # noqa: E402
REPLAY = ROOT / "replay"
DEFAULT_APPID = "1450243039"
DEFAULT_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{DEFAULT_APPID}/web_save"
MALL_API_URL = "https://storeapi.pay.qq.com/api/CommonCallMpgo"
ORDER_FIELDS = ["token_id", "openid", "openkey", "session_id", "session_type", "zoneid",
"pay_method", "buy_quantity", "mb_pwd", "pay_id", "auth_key",
"card_value", "accounttype", "provide_uin", "extend", "ts",
@@ -367,7 +369,7 @@ def cmd_mall_submit(args) -> int:
body_obj["call_param"]["call_param_json"] = json.dumps(cpj, ensure_ascii=False)
new_body = json.dumps(body_obj, ensure_ascii=False)
url = "https://pagedooapi.pay.qq.com/api/CommonCallMpgo?t=" + str(int(time.time() * 1000))
url = MALL_API_URL + "?t=" + str(int(time.time() * 1000))
cookie_str = "; ".join(f"{k}={v}" for k, v in cookies.items())
# midas 域 cookie 兜底(profile 导出常缺 midas_openid/midas_openkey,值为 openid/accesstoken)
if "midas_openid" not in cookies and "openid" in cookies:
@@ -552,7 +554,7 @@ def mall_getpaytoken(cookies: dict) -> tuple[list, str]:
"login_check_param_json": json.dumps(login),
},
}
url = "https://pagedooapi.pay.qq.com/api/CommonCallMpgo?t=" + str(int(time.time() * 1000))
url = MALL_API_URL + "?t=" + str(int(time.time() * 1000))
cookie_str = "; ".join(f"{k}={v}" for k, v in cookies.items())
for k, v in [("midas_openid", "openid"), ("midas_openkey", "accesstoken")]:
if k not in cookies and v in cookies:
@@ -690,7 +692,7 @@ def cmd_mall_auto(args) -> int:
for k, v in [("midas_openid", "openid"), ("midas_openkey", "accesstoken")]:
if k not in cookies and v in cookies:
cookie_str += f"; {k}=" + cookies[v]
url = "https://pagedooapi.pay.qq.com/api/CommonCallMpgo?t=" + str(int(time.time() * 1000))
url = MALL_API_URL + "?t=" + str(int(time.time() * 1000))
req = urllib.request.Request(url, data=new_body.encode("utf-8"), headers={
"Content-Type": "application/json", "Accept": "application/json, text/plain, */*",
"Origin": "https://z.iwan.yyb.qq.com", "Referer": "https://z.iwan.yyb.qq.com/",
@@ -722,6 +724,7 @@ def cmd_mall_auto(args) -> int:
return 0
detail = js.get("result_info", "") or call_reply.get("result_info", "") or ""
print(f"❌ 下单失败 ret={ret} inner={inner_ret} ({detail})")
print(f" {describe_payment_failure(str(cookies.get('logintype', '')), 'order', f'{ret} {inner_ret} {detail}')}")
print(f" 响应已保存 → {out}")
return 1
@@ -0,0 +1,46 @@
import fs from 'node:fs';
import { JSDOM, ResourceLoader } from 'jsdom';
import { patchEnvironment } from './scripts/jsdom-patch-env.mjs';
const ROOT = '/Users/yml/codes/douyu_login_py';
const out = ROOT + '/data/yyb-worker-dev/49684f85543d4fd1/jsdom-order';
const goodsUrl = JSON.parse(fs.readFileSync(`${out}/device-fp.json`, 'utf8')).goods_url;
const captured = [];
const requests = [];
const dom = new JSDOM(fs.readFileSync(`${out}/goods.html`, 'utf8'), {
url: goodsUrl,
referrer: 'https://z.iwan.yyb.qq.com/',
pretendToBeVisual: true,
runScripts: 'dangerously',
resources: new ResourceLoader(),
beforeParse(window) {
patchEnvironment(window);
const NativeXHR = window.XMLHttpRequest;
window.XMLHttpRequest = class extends NativeXHR {
open(method, url, async = true) { this.__m = method; this.__u = String(url); return super.open(method, url, async); }
send(body) {
const name = this.__u.includes('web_save') ? 'web_save'
: this.__u.includes('web_page_info') ? 'web_page_info'
: this.__u.includes('fp-behv') ? 'fp_behv' : 'other';
requests.push({ name, url: this.__u.slice(0, 120), bodyLen: String(body || '').length });
if (name === 'web_save') captured.push(String(body || ''));
this.readyState = 4; this.status = 200;
this.responseText = '{"ret":0,"msg":"","info":{}}';
if (typeof this.onreadystatechange === 'function') this.onreadystatechange();
if (typeof this.onload === 'function') this.onload();
}
};
},
});
await new Promise(r => setTimeout(r, 15000));
console.log('=== 页面发出的 XHR ===');
for (const r of requests) console.log(' ', r.name, '|', r.url, '| bodyLen:', r.bodyLen);
console.log('=== web_save body 捕获:', captured.length ? 'YES len=' + captured[0].length : 'NO');
console.log('=== 页面按钮 ===');
const btns = dom.window.document.querySelectorAll('[class*="btn"],[class*="confirm"],[class*="submit"],button');
for (const b of btns) {
const t = (b.textContent || '').trim().replace(/\s+/g, ' ');
if (t && t.length < 40 && b.offsetParent !== null) console.log(' btn:', t.slice(0, 30));
}
+36 -5
View File
@@ -1,10 +1,12 @@
"""encrypt_msg (web_new_encrypt) 生成器 — goodsBiz CHAOS VM 的纯 Python 移植
算法背景(见 FINDINGS.md F-2038..F-2043):
算法背景(见 FINDINGS.md F-2038..F-2043,破解见 F-2052/2026-08-12):
- goodsBiz.js 是腾讯 __TENCENT_CHAOS_VM 解释器(116 opcode,0..115)+ 178093 元素字节码。
- 加密核心 = webSave h@85091(18 参):表生成 → 检查 window.xMidasOps → 33 次块变换
(T 表 AES 类)→ 回调 h@69667(4 参)→ randGen h@85173(16 次重随机 key2)。
- 输入:18 参(key1 诱饵 / key2 真实 / args[10]=528B 明文缓冲 / 常量表 / 回调)。
- 输入:18 参(key1 诱饵 / key2 真实 / args[10]=16 字节对齐的明文缓冲 / 常量表 / 回调)。
- key 派生已破解:key16 = Sbox[Te 链(key1)];服务端校验两者派生一致性,
离线用随机 key16 + derive_key1_from_key16 反解 key1 即可通过(无需页面捕获 key)。
- 输出:C[3476] 528 字节密文(hex 1056 字符,头部 5574bea9... 固定前缀)。
本文件复刻解释器语义,可在无浏览器环境运行;两组独立真实向量均 1056/1056 逐字节一致。
@@ -660,6 +662,14 @@ class VM:
except Exception:
parts.append("??")
self.out_hex = "".join(parts)
# 仅供离线排查 webSave 输出缓冲何时变化,生产路径默认不启用。
if getattr(self, "capture_output_lengths", False):
v = js_index(C, 3476)
if isinstance(v, list):
length = len(v)
if length != getattr(self, "_last_output_length", None):
self._last_output_length = length
self.output_length_trace.append((u, op, length))
# ---- opcode dispatch ----
if op == 0:
a, b, c = o[u+1], o[u+2], o[u+3]; u += 3
@@ -1461,15 +1471,18 @@ def derive_key1_from_key16(key16, te_tables=None, sbox=None):
def generate_encrypt_msg_offline(params, fk_extend, ts, rand_val,
key16=None, key1=None,
args_template=None, cb_json=None,
random_seed=None, xmidas=None, xmidas_token=None):
random_seed=None, xmidas=None, xmidas_token=None,
diagnostics=None):
"""纯离线生成 encrypt_msg(A2 路线,2026-08-10 双向量 E3 闭环)。
params: 订单字段字典(18 字段,见 build_plaintext);fk_extend: tdrc_session 串
(最终编码形态 tdrc_session%3Dpay-...,不再二次编码);ts: 秒级时间戳;
rand_val: _rand 值(页面实测 8 字母数字 + 3×\\x03(run A)或 7 字母数字 +
4×\\x04(run B),调用方自定即可);
key16/key1: 16 字节密钥,**不影响最终输出**(E3:双向量随机密钥 1056/1056
复现;webSave 回调内部重派生密钥,机制见 U-2030);
key16/key1: 16 字节密钥,**不影响最终输出密文字节**(E3:双向量随机密钥 1056/1056
复现;webSave 回调内部重派生密钥),但服务端会校验 key16 与 key1 的派生一致性
(key16=Sbox[Te 链(key1)],F-2052),key1 必须由 derive_key1_from_key16 从 key16
反解,不能随意给定;
args_template: 常量表模板(跨次恒定,缺省用 work/replay/e2e/ws-args.json)。
"""
import random as _r
@@ -1523,6 +1536,13 @@ def generate_encrypt_msg_offline(params, fk_extend, ts, rand_val,
# 参数全部以 a:1 包装传入;key16 按全局块索引轮转:out[j] = word_byte[j] ^ key16[(4*blk+j)%16]
pt = build_plaintext(params, fk_extend, ts, rand_val)
pb = pt.encode("latin-1")
# goods VM 只逐个处理完整的 16 字节块,不会为尾块自动填充。
# 未对齐时继续执行会静默丢弃尾部字段,必须在请求前终止。
if len(pb) % 16:
raise ValueError(
f"webSave 明文必须为 16 字节对齐,当前 {len(pb)} 字节;"
"请用 _rand 补齐后再生成 encrypt_msg"
)
def _cont(*a):
return None
te_cap = json.loads(json.loads((replay / "e2e" / "multi-1.json").read_text().strip()))
@@ -1562,7 +1582,18 @@ def generate_encrypt_msg_offline(params, fk_extend, ts, rand_val,
vm.init_c = init_c
vm.capture_at_return = True
vm.out_hex = None
if diagnostics is not None:
vm.capture_output_lengths = True
vm.output_length_trace = []
vm._last_output_length = None
vm.run(web, [])
if vm.out_hex is None:
raise RuntimeError("webSave capture failed")
if diagnostics is not None:
diagnostics.update({
"plaintext_length": len(pb),
"transform_output_length": len(outbuf),
"ciphertext_length": len(vm.out_arr),
"output_length_trace": vm.output_length_trace,
})
return vm.out_hex
@@ -0,0 +1,31 @@
"""YYB 下单和付款失败响应的统一归类。"""
from __future__ import annotations
def describe_payment_failure(provider: str, phase: str, response: object) -> str:
"""根据已保存的响应内容返回不会诱导连续重试的失败说明。"""
text = str(response)
is_qq = provider.lower() in {"qq", "qc"}
if "1099-1007-0" in text or "ret=1099" in text:
if is_qq:
return (
"QQ 支付请求被风控/限流暂时拦截(1099-1007-0),未生成微信付款码;"
"请停止连续重试,等待 30-60 分钟后再使用新订单尝试。"
)
return "支付请求被暂时拦截(1099-1007-0),未生成微信付款码;请稍后使用新订单重试。"
if "10007" in text:
if is_qq:
return (
"QQ 商城下单被支付侧暂时拒绝(10007 系统繁忙),未创建可付款订单;"
"请停止连续重试,等待 30-60 分钟后再尝试。"
)
return "商城下单被暂时拒绝(10007 系统繁忙),未创建可付款订单;请稍后重试。"
if "10014" in text or "gamerole err" in text:
return "角色或区服校验失败(10014),未创建可付款订单;请重新选择角色和区服。"
if phase == "order":
return "商城订单创建失败,未创建可付款订单;请查看任务日志中的业务码。"
return "微信付款码生成失败;请查看任务日志中的业务码。"
@@ -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
@@ -15,6 +15,8 @@ _MODULE = importlib.util.module_from_spec(_SPEC)
sys.modules[_SPEC.name] = _MODULE
_SPEC.loader.exec_module(_MODULE)
from pyvm.payment_errors import describe_payment_failure
class TestJsdomPay(unittest.TestCase):
def test_build_save_body_has_current_payment_context(self):
@@ -42,8 +44,51 @@ class TestJsdomPay(unittest.TestCase):
fields = parse_qs(body, keep_blank_values=True)
self.assertEqual(fields["session_id"], ["openid"])
self.assertEqual(fields["session_type"], ["kp_accesstoken"])
self.assertEqual(fields["qq_appid"], ["102033112"])
self.assertEqual(fields["wx_appid"], [""])
self.assertEqual(fields["offerid_for_qq_appid"], ["102033112"])
self.assertNotIn("qq_appid", fields)
def test_wechat_payment_does_not_include_qq_offer_field(self):
body = _MODULE.build_save_body(
{"token_id": "token", "transaction_id": "transaction", "out_trade_no": "trade"},
{"openid": "openid", "accesstoken": "access-token", "logintype": "WX"},
"web-token", "anti-token", "cipher", "1", "pf", 100,
)
fields = parse_qs(body, keep_blank_values=True)
self.assertNotIn("offerid_for_qq_appid", fields)
def test_qq_page_info_matches_the_web_flow(self):
body = _MODULE.build_page_info_body(
{"token_id": "token", "transaction_id": "transaction", "out_trade_no": "trade"},
{"openid": "openid", "accesstoken": "access-token", "logintype": "QC", "appid": "102033112"},
"anti-token", "1", "pf",
)
fields = parse_qs(body, keep_blank_values=True)
self.assertEqual(fields["isusempaymode"], ["1"])
self.assertEqual(fields["offerid_for_qq_appid"], ["102033112"])
self.assertNotIn("pay_method", fields)
def test_qq_risk_rejection_is_not_reported_as_qr_error(self):
message = describe_payment_failure("QC", "payment", "ret=1099 err_code=1099-1007-0")
self.assertIn("风控/限流", message)
self.assertIn("未生成微信付款码", message)
self.assertIn("停止连续重试", message)
def test_game_role_error_is_classified_separately(self):
message = describe_payment_failure("QC", "order", "inner=10014 gamerole err")
self.assertIn("角色或区服校验失败", message)
self.assertIn("重新选择角色和区服", message)
def test_encrypt_rand_aligns_plaintext_to_aes_block(self):
params = {
"token_id": "t", "openid": "o", "openkey": "k", "session_id": "openid",
"session_type": "kp_accesstoken", "zoneid": "1", "pay_method": "wechat",
"buy_quantity": "1", "from_h5": "1", "webversion": "minipayv2",
}
rand_value = _MODULE.make_encrypt_rand(params, "tdrc_session%3Dpay-test", "1775990000")
plaintext = _MODULE.build_plaintext(params, "tdrc_session%3Dpay-test", "1775990000", rand_value)
self.assertEqual(len(plaintext.encode("latin-1")) % 16, 0)
self.assertGreaterEqual(len(rand_value), 8)
if __name__ == "__main__":