# -*- coding: utf-8 -*- """差分对照:/v3/closeorder 到底有没有真的改变 H5 单的状态? 背景:LsyOrderService.java:334 的注释断言 「上游『H5支付』目录下无独立关单接口,本类不为 H5 提供关单路径(见 spec §2.3)」 但第三轮实测:对一笔 H5 单发 /v3/closeorder(trade_no 寻址)→ 回「关单成功」, 随后 queryorder 由 PAY_IN_PROCESS「用户支付中」变成 PAY_FAIL「支付失败」。 「关单成功」四个字**不足以**证明状态被改了 —— 也可能它只是应了一声。 所以做差分:**同一时刻建两笔同参 H5 单,只关其中一笔**,再各查一次。 A 被关 → 若 A=PAY_FAIL 且 B=PAY_IN_PROCESS ⇒ 差分成立,closeorder 真的改了状态。 A 被关 → 若 A 与 B 同为 PAY_IN_PROCESS ⇒ 那个「关单成功」是空话(或尚未生效)。 两笔都 FAIL ⇒ 与关单无关(例如该商户的 H5 单本来就迅速过期),此对照组作废。 聚合码侧同法再做一组(/v3/qrclose 的对照),因为那是我方**已在用**的关单腿。 """ import io import json import os import re import time import uuid import urllib.error import urllib.request from hashlib import md5 YML = "java/src/main/resources/application.yml" VAR = re.compile(r"^\$\{([A-Za-z0-9_]+):(.*)\}$") OUTDIR = "logs/_lsy-ctl" OUT = "logs/_lsy-close-control.txt" buf = [] def say(s=""): buf.append(s) print(s.encode("ascii", "replace").decode()) def read_block(path, top): lines = io.open(path, encoding="utf-8", errors="replace").read().split("\n") start = None for i, ln in enumerate(lines): if re.match(r"^%s:\s*$" % re.escape(top), ln): start = i break if start is None: raise SystemExit("找不到顶层段 %s:" % top) out = {} for j in range(start + 1, len(lines)): ln = lines[j] if ln.strip() == "" or ln.lstrip().startswith("#"): continue if not ln.startswith((" ", "\t")): break m = re.match(r"^\s+([A-Za-z0-9_.\-]+):\s*(.*)$", ln) if m: out[m.group(1)] = re.split(r"\s+#", m.group(2), maxsplit=1)[0].strip().strip("'\"") return out def resolve(raw): m = VAR.match(raw) if not m: return raw return os.environ.get(m.group(1)) or m.group(2) cfg = read_block(YML, "leshouying") BASE = resolve(cfg["base-url"]).rstrip("/") INST = resolve(cfg["inst-no"]) KEY = resolve(cfg["key"]) MCH = "Y52211090004" if not os.path.isdir(OUTDIR): os.makedirs(OUTDIR) def call(path, params, tag, timeout=60): p = dict(params) p.setdefault("inst_no", INST) p.setdefault("mch_no", MCH) A = "&".join("%s=%s" % (k, p[k]) for k in sorted(p) if p[k] is not None) p["sign"] = md5((A + "&key=" + KEY).encode("utf-8")).hexdigest().lower() body = json.dumps(p, ensure_ascii=False).encode("utf-8") req = urllib.request.Request(BASE + path, data=body, headers={"Content-Type": "application/json;charset=utf-8"}, method="POST") try: with urllib.request.urlopen(req, timeout=timeout) as r: raw = r.read() except urllib.error.HTTPError as e: raw = e.read() except Exception as e: # noqa: BLE001 say(" !! 量具失效 %s: %r" % (tag, e)) return None io.open("%s/%s.bin" % (OUTDIR, tag), "wb").write(raw) resp = json.loads(raw.decode("utf-8")) usign = resp.get("sign") vs = "sign=null" if usign: nn = {k: v for k, v in resp.items() if k != "sign" and v is not None} A2 = "&".join("%s=%s" % (k, nn[k]) for k in sorted(nn)) vs = ("验签OK(%d)" % len(nn) if md5((A2 + "&key=" + KEY).encode("utf-8")).hexdigest().lower() == usign else "验签FAIL") say(" %-16s %-18s rc=%-8s ec=%-14s %s | %s" % (tag, path, resp.get("return_code"), resp.get("result_code"), resp.get("return_msg"), vs)) return resp def stamp(): return time.strftime("%Y%m%d%H%M%S") def uid(p): return "%s%s%s" % (p, time.strftime("%H%M%S"), uuid.uuid4().hex[:6]) def q(tag, **kw): r = call("/v3/queryorder", dict(query_trace_no=uuid.uuid4().hex, **kw), tag) return r # ══════════════════════ 对照一:H5 + /v3/closeorder ══════════════════════ say("=" * 78) say("对照一:两笔同参 H5,只关 A —— 判 /v3/closeorder 是否真改状态") say("=" * 78) ta, pa = uid("hA"), stamp() ra = call("/v3/h5pay", {"pay_trace_no": ta, "pay_time": pa, "total_amount": "1", "pay_type": "300", "order_body": "对照-H5A-待关"}, "h5-A-create") tb, pb = uid("hB"), stamp() rb = call("/v3/h5pay", {"pay_trace_no": tb, "pay_time": pb, "total_amount": "1", "pay_type": "300", "order_body": "对照-H5B-不动"}, "h5-B-create") A = ra.get("trade_no") if ra else None B = rb.get("trade_no") if rb else None say(" A.trade_no = %s" % A) say(" B.trade_no = %s" % B) say() say(" ── 关单前的基线(两笔都必须是 PAY_IN_PROCESS,否则对照作废)──") qa0 = q("A-baseline", trade_no=A) qb0 = q("B-baseline", trade_no=B) say() say(" ── 只关 A ──") call("/v3/closeorder", {"close_trace_no": uid("kA"), "close_time": stamp(), "trade_no": A}, "close-A") say() say(" ── 关后各查一次 ──") qa1 = q("A-after", trade_no=A) qb1 = q("B-after", trade_no=B) say() say(" 判决:") say(" A: %s -> %s (%s)" % (qa0.get("result_code") if qa0 else "?", qa1.get("result_code") if qa1 else "?", qa1.get("return_msg") if qa1 else "?")) say(" B: %s -> %s (%s)" % (qb0.get("result_code") if qb0 else "?", qb1.get("result_code") if qb1 else "?", qb1.get("return_msg") if qb1 else "?")) if qa1 and qb1 and qb1.get("result_code") == "PAY_IN_PROCESS" and qa1.get("result_code") == "PAY_FAIL": say(" ⇒ **差分成立**:closeorder 真把 A 从「用户支付中」改成了「支付失败」,") say(" 而同一刻同参的 B 仍是「用户支付中」。closeorder 对 H5 有效。") elif qa1 and qb1 and qa1.get("result_code") == qb1.get("result_code"): say(" ⇒ 两笔同态 ⇒ closeorder 对 H5 **没有改变状态**(或那个「关单成功」是空话)。") else: say(" ⇒ 读数不可判,见上表。") say() # ══════════════════════ 对照二:聚合码 + /v3/qrclose ══════════════════════ say("=" * 78) say("对照二:两笔同参聚合码,只关 A —— 复核我方已在用的 /v3/qrclose") say("=" * 78) ta2, pa2 = uid("qA"), stamp() ra2 = call("/v3/qrpay", {"pay_trace_no": ta2, "pay_time": pa2, "total_amount": "1", "time_expire": "30", "order_body": "对照-码A-待关"}, "qr-A-create") tb2, pb2 = uid("qB"), stamp() rb2 = call("/v3/qrpay", {"pay_trace_no": tb2, "pay_time": pb2, "total_amount": "1", "time_expire": "30", "order_body": "对照-码B-不动"}, "qr-B-create") UA = ra2.get("code_url") if ra2 else None say(" A.code_url = %s" % UA) say(" B.code_url = %s" % (rb2.get("code_url") if rb2 else None)) say() say(" ── 关单前的基线(已知:未扫的聚合码单查不到,此处复核)──") qa2 = q("A-qr-baseline", pay_trace_no=ta2, pay_time=pa2) qb2 = q("B-qr-baseline", pay_trace_no=tb2, pay_time=pb2) say() say(" ── 只关 A ──") call("/v3/qrclose", {"code_url": UA}, "qrclose-A") say() say(" ── 关后各查一次 ──") qa3 = q("A-qr-after", pay_trace_no=ta2, pay_time=pa2) qb3 = q("B-qr-after", pay_trace_no=tb2, pay_time=pb2) say(" 判决:A 关前/关后 = %s/%s ;B 关前/关后 = %s/%s" % (qa2.get("return_code") if qa2 else "?", qa3.get("return_code") if qa3 else "?", qb2.get("return_code") if qb2 else "?", qb3.get("return_code") if qb3 else "?")) say(" (聚合码单在关前关后都查不到 ⇒「查不到」是这个单**种类**的性质,") say(" 与关单动作无关 —— 正好反证第一轮那条负读数不是关单造的。)") say() say("句柄留档:") say(" h5 A/B = %s / %s (A 已关,B 未动,可用于后续比对)" % (A, B)) say(" qr A/B = %s / %s" % (ta2, tb2)) io.open(OUT, "w", encoding="utf-8").write("\n".join(buf) + "\n") print("report -> %s" % OUT)