| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- # -*- 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)
|