# -*- coding: utf-8 -*- """第三轮:① 平安(50)渠道的退款解锁重试;② 关单腿铺开;③ 若干收尾读数。 第二轮遗留: * supply-ch50 建单成功(200001070J1226092816253210006)但**查询超时** —— 量具失效, 不是负读数,必须重跑。 * /v3/cancelorder 对真实被扫失败单回「暂不支持撤销!」(sign 有效)—— 待确认是不是 只对这一笔/这一个渠道成立。 """ 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-lc3" OUT = "logs/_lsy-round3.txt" buf = [] rows = [] 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, envelope="v3", 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: code, raw = r.status, r.read() except urllib.error.HTTPError as e: code, raw = e.code, e.read() except Exception as e: # noqa: BLE001 say(" !! 未完成 %s: %r ← 量具失效,不是上游回答" % (tag, e)) rows.append((tag, path, "TRANSPORT_ERR", "量具失效")) return None io.open("%s/%s.bin" % (OUTDIR, tag), "wb").write(raw) try: resp = json.loads(raw.decode("utf-8")) except Exception: # noqa: BLE001 rows.append((tag, path, "NON_JSON", "%d B" % len(raw))) return None 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(%d)" % len(nn)) elif "sign" not in resp: vs = "无sign键" rc, ec = resp.get("return_code"), resp.get("result_code") if envelope == "legacy": rc, ec = resp.get("code"), resp.get("data") say(" POST %-22s -> %4d B rc=%s ec=%s %s" % (path, len(raw), rc, ec, vs)) say(" msg = %s" % (resp.get("return_msg") or resp.get("message"))) rows.append((tag, path, "%s/%s" % (rc, ec), "%s; %s" % (str(resp.get("return_msg") or resp.get("message"))[:44], 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]) # 从第二轮原始字节里读句柄,不手抄 ch50 = json.loads(io.open("logs/_lsy-lc2/supply-ch50.bin", "rb").read().decode("utf-8")) CH50_NO = ch50["data"] ch37_no = "200001070J1226092816241610001" BARCODE = "200001070X3126092816241710003" H5_CLOSED = "200001070X4326092816241610002" say("句柄(均读自原始应答字节)") say(" ch50(平安) 补录单 = %s" % CH50_NO) say(" ch37(建行) 补录单 = %s" % ch37_no) say(" 真实被扫失败单 = %s" % BARCODE) say(" 已关单的 H5 = %s" % H5_CLOSED) say() # ─────────────────────────────── 1) 平安渠道:查询 + 退款 say("=" * 78) say("【1】平安(50)渠道的补录单 —— 查询(第二轮超时,重跑)") say("=" * 78) q = call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex, "trade_no": CH50_NO}, "q-ch50-bytradeno") say() say("【1b】对它退款(先 trade_no 寻址)") call("/v3/refundorder", {"refund_trace_no": uid("p5"), "refund_fee": "1", "pay_time": stamp(), "trade_no": CH50_NO}, "refund-ch50-tradeno") say() # ─────────────────────────────── 2) 关单腿铺开 say("=" * 78) say("【2】关单:/v3/closeorder 全路径(我方**未实现**这个端点)") say("=" * 78) t, pt = uid("h5c"), stamp() h5 = call("/v3/h5pay", {"pay_trace_no": t, "pay_time": pt, "total_amount": "1", "pay_type": "300", "order_body": "三轮-关单用H5"}, "h5-for-close") NEW_H5 = h5.get("trade_no") if h5 else None say(" 新 H5 trade_no = %s" % NEW_H5) say() if NEW_H5: call("/v3/closeorder", {"close_trace_no": uid("k1"), "close_time": stamp(), "trade_no": NEW_H5}, "co-h5-1st") call("/v3/closeorder", {"close_trace_no": uid("k2"), "close_time": stamp(), "trade_no": NEW_H5}, "co-h5-2nd-幂等") call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex, "trade_no": NEW_H5}, "q-h5-afterclose") say() # 关单:两个键都不发 call("/v3/closeorder", {"close_trace_no": uid("k3"), "close_time": stamp()}, "co-nokey") # 关单:被扫单(文档说 closeorder 只用于二维码/JSAPI) call("/v3/closeorder", {"close_trace_no": uid("k4"), "close_time": stamp(), "trade_no": BARCODE}, "co-barcode") # 关单:已关过的老 H5 call("/v3/closeorder", {"close_trace_no": uid("k5"), "close_time": stamp(), "trade_no": H5_CLOSED}, "co-oldh5-again") # 关单:不存在的单 call("/v3/closeorder", {"close_trace_no": uid("k6"), "close_time": stamp(), "trade_no": "200001070X9999999999999999999"}, "co-nonexistent") say() # ─────────────────────────────── 3) 已关单的 H5 还能查到吗 say("=" * 78) say("【3】已关单的 H5 在 queryorder 里是什么样") say("=" * 78) call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex, "trade_no": H5_CLOSED}, "q-oldh5-closed") say() # ─────────────────────────────── 4) 退款:真实被扫失败单 / 不存在单 say("=" * 78) say("【4】退款错误路径补齐") say("=" * 78) call("/v3/refundorder", {"refund_trace_no": uid("rb"), "refund_fee": "1", "pay_time": stamp(), "trade_no": BARCODE}, "refund-barcode-unpaid") call("/v3/refundorder", {"refund_trace_no": uid("rz"), "refund_fee": "1", "pay_time": stamp(), "trade_no": "200001070X9999999999999999999"}, "refund-bogus-tradeno") say() # ─────────────────────────────── 5) 撤销:再试一笔,确认「暂不支持」是不是普遍 say("=" * 78) say("【5】撤销:换一笔再试(判「暂不支持撤销」是这一笔的还是普遍的)") say("=" * 78) t2, p2 = uid("bc2"), stamp() bc = call("/v3/pay", {"pay_trace_no": t2, "pay_time": p2, "total_amount": "1", "auth_code": "284812345678901234", "pay_type": "300", "order_body": "三轮-撤销用被扫"}, "barcode-for-cancel") if bc and bc.get("trade_no"): say(" 新被扫单 trade_no = %s" % bc["trade_no"]) call("/v3/cancelorder", {"cancel_trace_no": uid("c2"), "cancel_time": stamp(), "trade_no": bc["trade_no"]}, "cancel-barcode-2nd") call("/v3/cancelorder", {"cancel_trace_no": uid("c3"), "cancel_time": stamp(), "pay_trace_no": t2, "pay_time": p2}, "cancel-barcode-bytrace") say() say("=" * 78) say("汇总(%d 条)" % len(rows)) say("=" * 78) say("%-28s %-22s %-20s %s" % ("tag", "path", "rc/ec", "note")) for t_, p_, v, d in rows: say("%-28s %-22s %-20s %s" % (t_, p_, v, d)) io.open(OUT, "w", encoding="utf-8").write("\n".join(buf) + "\n") print("report -> %s" % OUT)