浏览代码

test(乐收赢): 全生命周期联调实测台与四轮读数(收单/查单/退款/关单/撤销/补录)

上一提交(91280dd)那条 H5 关单改判,依据就出在这批脚本里。放在这里是为了让
javadoc 里那句「读数见 logs/_lsy-close-control.txt」有东西可指 —— 不然引用是悬空的。

四个脚本,各自对应一轮,跑完把读数写成同名的 .txt,原始应答字节落在同名目录:

  _lsy-lifecycle.py    / _lsy-lc/    第一轮,30 条:六条腿各铺一遍
  _lsy-refund-unlock.py/ _lsy-lc2/   第二轮,退款解锁尝试
  _lsy-round3.py       / _lsy-lc3/   第三轮,关单腿铺开 + 量具失效重跑
  _lsy-close-control.py/ _lsy-ctl/   **差分对照**,H5 关单的决定性取证

每条应答都存了原始字节(.bin),且逐条做过入站验签(null 排除读法)——
.test 里写的「验签OK(n)」是当场用机构密钥复算出来的,不是我转述的。

四轮下来的主要读数:

1. 关单:/v3/closeorder 对 H5 有效(差分对照见上一条提交)。
   幂等第二次回「关单失败:该订单交易失败,不支持关单」;对不存在的单回
   「订单信息不存在!」(return_code=FAIL,sign=null)。
   已关的 H5 再查,queryorder 回 PAY_FAIL/「支付失败」——**关掉的单与支付失败的单
   在这一层不可分辨**,这是上游的语义,我方无处可改。

2. 未扫的聚合码单,queryorder 一律「订单信息不存在!」。
   这是**单的种类**的性质,不是关单动作造的:差分对照里 A、B 两笔在关单**前后**
   都查不到。⇒ 对账腿每 5 分钟会把这种正常业务回答记成带栈 ERROR(另案,未改)。

3. 退款:**成功路径在本环境测不出来** —— 造不出一笔真正已支付的单。
   /v3/createSupply 能补出一条 queryorder 报 PAY_SUCCESS 的记录,但对它退款,
   pay_trace_no+pay_time 与 trade_no 两种寻址都回「退款失败:没有查询到符合条件的记录。」;
   平安(50)渠道那条补录单则回 PAY_IN_PROCESS/「系统异常」。没有真实付款人,
   造不出已收款的单。退款**错误**路径(不存在/未付/已关/金额超限)都跑过了。

4. /v3/cancelorder 在本商户被上游一律拒绝(四次「暂不支持撤销!」)⇒ 被扫撤销腿是死的。
   属上游商户配置,本轮**不动**,已在生产码注释里记明。

5. /v3/queryorder 接受只发 pay_trace_no、**不发** pay_time(文档说二者二选一必传)。

6. /v3/queryrefund 的 refund_time 必须填**退款请求时刻**(退款应答回显的那个 pay_time),
   填查询时刻会回「订单信息不存在!」—— 这一条我踩过一次,是**我的量具错**,
   不是上游的负读数,已在脚本里改正。

另外两个小脚本:
  _lsy-key-scan.py   提交前体检:扫这批产物里有没有混进机构密钥。只打计数与文件名。
                     带正对照(密钥必然在 application.yml 里,扫不到就说明扫描器是坏的)。
                     读数:密钥长度 32;正对照命中 1;扫描 79 个文件,含密钥 0 个。

已知未覆盖(不假装覆盖了):
  * 顾客**正在**付款的那一瞬间调关单(无真实付款人,造不出这个时刻);
  * 任何一笔真实的支付成功 → 退款成功(同上);
  * 联调环境之外的环境(全部读数都来自 test.gdyfsk.com,inst_no=900112)。
alphaH 2 周之前
父节点
当前提交
ed310fcb1d
共有 80 个文件被更改,包括 1640 次插入 和 0 次删除
  1. 204 0
      logs/_lsy-close-control.py
  2. 50 0
      logs/_lsy-close-control.txt
  3. 1 0
      logs/_lsy-ctl/A-after.bin
  4. 1 0
      logs/_lsy-ctl/A-baseline.bin
  5. 1 0
      logs/_lsy-ctl/A-qr-after.bin
  6. 1 0
      logs/_lsy-ctl/A-qr-baseline.bin
  7. 1 0
      logs/_lsy-ctl/B-after.bin
  8. 1 0
      logs/_lsy-ctl/B-baseline.bin
  9. 1 0
      logs/_lsy-ctl/B-qr-after.bin
  10. 1 0
      logs/_lsy-ctl/B-qr-baseline.bin
  11. 1 0
      logs/_lsy-ctl/close-A.bin
  12. 1 0
      logs/_lsy-ctl/h5-A-create.bin
  13. 1 0
      logs/_lsy-ctl/h5-B-create.bin
  14. 1 0
      logs/_lsy-ctl/qr-A-create.bin
  15. 1 0
      logs/_lsy-ctl/qr-B-create.bin
  16. 1 0
      logs/_lsy-ctl/qrclose-A.bin
  17. 73 0
      logs/_lsy-key-scan.py
  18. 1 0
      logs/_lsy-lc/cancel-nonexistent.bin
  19. 1 0
      logs/_lsy-lc/close-qr-1st.bin
  20. 1 0
      logs/_lsy-lc/close-qr-2nd.bin
  21. 1 0
      logs/_lsy-lc/close-qr-nocodeurl.bin
  22. 1 0
      logs/_lsy-lc/close-qr-nonexistent.bin
  23. 1 0
      logs/_lsy-lc/closeorder-h5-bytradeno.bin
  24. 1 0
      logs/_lsy-lc/closeorder-qr-bytrace.bin
  25. 1 0
      logs/_lsy-lc/create-h5pay.bin
  26. 1 0
      logs/_lsy-lc/create-pay-badcode.bin
  27. 1 0
      logs/_lsy-lc/create-pay-barcode.bin
  28. 1 0
      logs/_lsy-lc/create-qrpay-dup.bin
  29. 1 0
      logs/_lsy-lc/create-qrpay-full.bin
  30. 1 0
      logs/_lsy-lc/create-qrpay-min.bin
  31. 1 0
      logs/_lsy-lc/query-h5-both-keys.bin
  32. 1 0
      logs/_lsy-lc/query-h5-bytrace.bin
  33. 1 0
      logs/_lsy-lc/query-h5-bytradeno.bin
  34. 1 0
      logs/_lsy-lc/query-h5-traceONLY.bin
  35. 1 0
      logs/_lsy-lc/query-nokey.bin
  36. 1 0
      logs/_lsy-lc/query-nonexistent.bin
  37. 1 0
      logs/_lsy-lc/query-qrpay-afterclose.bin
  38. 1 0
      logs/_lsy-lc/query-qrpay-unscanned.bin
  39. 1 0
      logs/_lsy-lc/query-supply-bytrace.bin
  40. 1 0
      logs/_lsy-lc/query-supply-bytradeno.bin
  41. 1 0
      logs/_lsy-lc/queryrefund-byno.bin
  42. 1 0
      logs/_lsy-lc/queryrefund-bytrace.bin
  43. 1 0
      logs/_lsy-lc/refund-nonexistent.bin
  44. 1 0
      logs/_lsy-lc/refund-overamount.bin
  45. 1 0
      logs/_lsy-lc/refund-supply-bytrace.bin
  46. 1 0
      logs/_lsy-lc/refund-unpaid-qrpay.bin
  47. 1 0
      logs/_lsy-lc/supply-1.bin
  48. 1 0
      logs/_lsy-lc2/cancel-barcode-bytradeno.bin
  49. 1 0
      logs/_lsy-lc2/q-barcode-aftercancel.bin
  50. 1 0
      logs/_lsy-lc2/qrefund-byno-again.bin
  51. 1 0
      logs/_lsy-lc2/qrefund-bytrace-fixed.bin
  52. 1 0
      logs/_lsy-lc2/refund-closed-h5.bin
  53. 1 0
      logs/_lsy-lc2/refund-tradeno-610001.bin
  54. 1 0
      logs/_lsy-lc2/supply-ch1.bin
  55. 1 0
      logs/_lsy-lc2/supply-ch2.bin
  56. 1 0
      logs/_lsy-lc2/supply-ch4.bin
  57. 1 0
      logs/_lsy-lc2/supply-ch50.bin
  58. 1 0
      logs/_lsy-lc2/supply-ch6.bin
  59. 1 0
      logs/_lsy-lc2/supply-flag2.bin
  60. 1 0
      logs/_lsy-lc3/barcode-for-cancel.bin
  61. 1 0
      logs/_lsy-lc3/cancel-barcode-2nd.bin
  62. 1 0
      logs/_lsy-lc3/cancel-barcode-bytrace.bin
  63. 1 0
      logs/_lsy-lc3/co-barcode.bin
  64. 1 0
      logs/_lsy-lc3/co-h5-1st.bin
  65. 1 0
      logs/_lsy-lc3/co-h5-2nd-幂等.bin
  66. 1 0
      logs/_lsy-lc3/co-nokey.bin
  67. 1 0
      logs/_lsy-lc3/co-nonexistent.bin
  68. 1 0
      logs/_lsy-lc3/co-oldh5-again.bin
  69. 1 0
      logs/_lsy-lc3/h5-for-close.bin
  70. 1 0
      logs/_lsy-lc3/q-ch50-bytradeno.bin
  71. 1 0
      logs/_lsy-lc3/q-h5-afterclose.bin
  72. 1 0
      logs/_lsy-lc3/q-oldh5-closed.bin
  73. 1 0
      logs/_lsy-lc3/refund-barcode-unpaid.bin
  74. 1 0
      logs/_lsy-lc3/refund-bogus-tradeno.bin
  75. 419 0
      logs/_lsy-lifecycle.py
  76. 137 0
      logs/_lsy-lifecycle.txt
  77. 279 0
      logs/_lsy-refund-unlock.py
  78. 99 0
      logs/_lsy-refund-unlock.txt
  79. 225 0
      logs/_lsy-round3.py
  80. 83 0
      logs/_lsy-round3.txt

+ 204 - 0
logs/_lsy-close-control.py

@@ -0,0 +1,204 @@
+# -*- 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)

+ 50 - 0
logs/_lsy-close-control.txt

@@ -0,0 +1,50 @@
+==============================================================================
+对照一:两笔同参 H5,只关 A —— 判 /v3/closeorder 是否真改状态
+==============================================================================
+  h5-A-create      /v3/h5pay          rc=SUCCESS  ec=PAY_SUCCESS    预支付成功 | 验签OK(13)
+  h5-B-create      /v3/h5pay          rc=SUCCESS  ec=PAY_SUCCESS    预支付成功 | 验签OK(13)
+  A.trade_no = 200001070X4326092816295710013
+  B.trade_no = 200001070X4326092816295710014
+
+  ── 关单前的基线(两笔都必须是 PAY_IN_PROCESS,否则对照作废)──
+  A-baseline       /v3/queryorder     rc=SUCCESS  ec=PAY_IN_PROCESS 用户支付中 | 验签OK(17)
+  B-baseline       /v3/queryorder     rc=SUCCESS  ec=PAY_IN_PROCESS 用户支付中 | 验签OK(17)
+
+  ── 只关 A ──
+  close-A          /v3/closeorder     rc=SUCCESS  ec=PAY_SUCCESS    关单成功 | 验签OK(8)
+
+  ── 关后各查一次 ──
+  A-after          /v3/queryorder     rc=SUCCESS  ec=PAY_FAIL       支付失败 | 验签OK(17)
+  B-after          /v3/queryorder     rc=SUCCESS  ec=PAY_IN_PROCESS 用户支付中 | 验签OK(17)
+
+  判决:
+    A: PAY_IN_PROCESS -> PAY_FAIL  (支付失败)
+    B: PAY_IN_PROCESS -> PAY_IN_PROCESS  (用户支付中)
+    ⇒ **差分成立**:closeorder 真把 A 从「用户支付中」改成了「支付失败」,
+      而同一刻同参的 B 仍是「用户支付中」。closeorder 对 H5 有效。
+
+==============================================================================
+对照二:两笔同参聚合码,只关 A —— 复核我方已在用的 /v3/qrclose
+==============================================================================
+  qr-A-create      /v3/qrpay          rc=SUCCESS  ec=PAY_SUCCESS    聚合码请求成功! | 验签OK(11)
+  qr-B-create      /v3/qrpay          rc=SUCCESS  ec=PAY_SUCCESS    聚合码请求成功! | 验签OK(11)
+  A.code_url = https://test.gdyfsk.com/yfpay/s/s-00006
+  B.code_url = https://test.gdyfsk.com/yfpay/s/s-00007
+
+  ── 关单前的基线(已知:未扫的聚合码单查不到,此处复核)──
+  A-qr-baseline    /v3/queryorder     rc=FAIL     ec=None           订单信息不存在! | 验签OK(2)
+  B-qr-baseline    /v3/queryorder     rc=FAIL     ec=None           订单信息不存在! | 验签OK(2)
+
+  ── 只关 A ──
+  qrclose-A        /v3/qrclose        rc=SUCCESS  ec=PAY_SUCCESS    聚合码主动关单成功! | 验签OK(7)
+
+  ── 关后各查一次 ──
+  A-qr-after       /v3/queryorder     rc=FAIL     ec=None           订单信息不存在! | 验签OK(2)
+  B-qr-after       /v3/queryorder     rc=FAIL     ec=None           订单信息不存在! | 验签OK(2)
+  判决:A 关前/关后 = FAIL/FAIL ;B 关前/关后 = FAIL/FAIL
+  (聚合码单在关前关后都查不到 ⇒「查不到」是这个单**种类**的性质,
+    与关单动作无关 —— 正好反证第一轮那条负读数不是关单造的。)
+
+句柄留档:
+  h5 A/B = 200001070X4326092816295710013 / 200001070X4326092816295710014   (A 已关,B 未动,可用于后续比对)
+  qr A/B = qA162958b7be9c / qB162958c4b4ee

+ 1 - 0
logs/_lsy-ctl/A-after.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"支付失败","result_code":"PAY_FAIL","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"60af5fa062a84411bb43f7527a934e14","pay_trace_no":"hA16295772ae7e","pay_time":"20260928162957","total_amount":"1","end_time":"20260928162958","trade_no":"200001070X4326092816295710013","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":"0","pay_amount":"1","coupon_amount":null,"discount_amount":"0","sign":"6dffae39128ce16b45953ce4bc702c8c"}

+ 1 - 0
logs/_lsy-ctl/A-baseline.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"用户支付中","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"fc0c4b5066de4d41913364e9de422d1d","pay_trace_no":"hA16295772ae7e","pay_time":"20260928162957","total_amount":"1","end_time":null,"trade_no":"200001070X4326092816295710013","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":"USERPAYING","receipt_amount":"0","pay_amount":"1","coupon_amount":null,"discount_amount":"0","sign":"58b0dfa638874aa8849164eedb640c3b"}

+ 1 - 0
logs/_lsy-ctl/A-qr-after.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"query_trace_no":null,"pay_trace_no":null,"pay_time":null,"total_amount":null,"end_time":null,"trade_no":null,"td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"a3cb8169d863e438486a6ea0ae72fea7"}

+ 1 - 0
logs/_lsy-ctl/A-qr-baseline.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"query_trace_no":null,"pay_trace_no":null,"pay_time":null,"total_amount":null,"end_time":null,"trade_no":null,"td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"a3cb8169d863e438486a6ea0ae72fea7"}

+ 1 - 0
logs/_lsy-ctl/B-after.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"用户支付中","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"eed946052f0b4f7881e99e2f97b65d54","pay_trace_no":"hB162957e2e9d9","pay_time":"20260928162957","total_amount":"1","end_time":null,"trade_no":"200001070X4326092816295710014","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":"USERPAYING","receipt_amount":"0","pay_amount":"1","coupon_amount":null,"discount_amount":"0","sign":"aa117bc031a9dee7191240e2146534ce"}

+ 1 - 0
logs/_lsy-ctl/B-baseline.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"用户支付中","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"8b9ecc9140484bcfbc799334e771fcbe","pay_trace_no":"hB162957e2e9d9","pay_time":"20260928162957","total_amount":"1","end_time":null,"trade_no":"200001070X4326092816295710014","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":"USERPAYING","receipt_amount":"0","pay_amount":"1","coupon_amount":null,"discount_amount":"0","sign":"42b07f370ff8ba17f4cc0578a34c2a01"}

+ 1 - 0
logs/_lsy-ctl/B-qr-after.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"query_trace_no":null,"pay_trace_no":null,"pay_time":null,"total_amount":null,"end_time":null,"trade_no":null,"td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"a3cb8169d863e438486a6ea0ae72fea7"}

+ 1 - 0
logs/_lsy-ctl/B-qr-baseline.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"query_trace_no":null,"pay_trace_no":null,"pay_time":null,"total_amount":null,"end_time":null,"trade_no":null,"td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"a3cb8169d863e438486a6ea0ae72fea7"}

+ 1 - 0
logs/_lsy-ctl/close-A.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"关单成功","result_code":"PAY_SUCCESS","mch_no":null,"close_trace_no":"kA162958b50495","close_time":"20260928162958","trade_no":"200001070X4326092816295710013","pay_trace_no":"hA16295772ae7e","pay_time":"20260928162957","sign":"c15271fdf0632354c35007020a4d1fd0"}

+ 1 - 0
logs/_lsy-ctl/h5-A-create.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"预支付成功","result_code":"PAY_SUCCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","pay_trace_no":"hA16295772ae7e","pay_time":"20260928162957","total_amount":"1","trade_no":"200001070X4326092816295710013","appId":null,"timeStamp":null,"nonceStr":null,"packages":null,"signType":null,"paySign":null,"ali_trade_no":null,"attach":null,"code_url":"https://yyfweb.postar.cn/postalpay/p/generateCode/cloud?threeOrderNo=200001070X4326092816295710013&agetId=FWH000012501&orderDate=20260928","sign":"0ca77f1ed563b0da9d08dd4219308d25","trade_state":null,"pay_amount":null,"coupon_amount":null,"receipt_amount":null,"discount_amount":null,"miniapp_data":null,"mem_data":null}

+ 1 - 0
logs/_lsy-ctl/h5-B-create.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"预支付成功","result_code":"PAY_SUCCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","pay_trace_no":"hB162957e2e9d9","pay_time":"20260928162957","total_amount":"1","trade_no":"200001070X4326092816295710014","appId":null,"timeStamp":null,"nonceStr":null,"packages":null,"signType":null,"paySign":null,"ali_trade_no":null,"attach":null,"code_url":"https://yyfweb.postar.cn/postalpay/p/generateCode/cloud?threeOrderNo=200001070X4326092816295710014&agetId=FWH000012501&orderDate=20260928","sign":"9eaeb47c24409fead703cc1261611880","trade_state":null,"pay_amount":null,"coupon_amount":null,"receipt_amount":null,"discount_amount":null,"miniapp_data":null,"mem_data":null}

+ 1 - 0
logs/_lsy-ctl/qr-A-create.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"聚合码请求成功!","result_code":"PAY_SUCCESS","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","pay_trace_no":"qA162958b7be9c","pay_time":"20260928162958","total_amount":"1","code_url":"https://test.gdyfsk.com/yfpay/s/s-00006","sign":"bbbb742d3fb050c6d4b2336033d628b0"}

+ 1 - 0
logs/_lsy-ctl/qr-B-create.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"聚合码请求成功!","result_code":"PAY_SUCCESS","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","pay_trace_no":"qB162958c4b4ee","pay_time":"20260928162958","total_amount":"1","code_url":"https://test.gdyfsk.com/yfpay/s/s-00007","sign":"e261743f481f583fe688b6756601cf1a"}

+ 1 - 0
logs/_lsy-ctl/qrclose-A.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"聚合码主动关单成功!","result_code":"PAY_SUCCESS","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","sign":"58b1cb3946a1dc8ef76182e462d74e0b"}

+ 73 - 0
logs/_lsy-key-scan.py

@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+"""提交前体检:logs/ 下的产物里有没有混进机构密钥。
+只打计数与文件名(文件名不是秘密),绝不打印密钥本身或长度以外的信息。"""
+import io, os, re, sys
+
+YML = "java/src/main/resources/application.yml"
+VAR = re.compile(r"^\$\{([A-Za-z0-9_]+):(.*)\}$")
+
+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
+
+cfg = read_block(YML, "leshouying")
+raw = cfg.get("key", "")
+m = VAR.match(raw)
+KEY = (os.environ.get(m.group(1)) or m.group(2)) if m else raw
+print("密钥长度 = %d(只打长度)" % len(KEY))
+if len(KEY) < 8:
+    raise SystemExit("密钥太短,无法作为搜索针,体检作废")
+
+needle = KEY.encode("utf-8")
+
+# 正对照:密钥必然在 application.yml 里;扫不到说明扫描器/针是坏的
+def hits(path):
+    try:
+        return io.open(path, "rb").read().count(needle)
+    except Exception:
+        return 0
+
+pos = hits(YML)
+print("正对照 application.yml 命中 = %d  %s" % (pos, "OK" if pos >= 1 else "!! 扫描器失效,体检作废"))
+if pos < 1:
+    raise SystemExit(1)
+
+dirs = ["logs/_lsy-lc", "logs/_lsy-lc2", "logs/_lsy-lc3", "logs/_lsy-ctl"]
+loose = ["logs/_lsy-lifecycle.py", "logs/_lsy-lifecycle.txt",
+         "logs/_lsy-refund-unlock.py", "logs/_lsy-refund-unlock.txt",
+         "logs/_lsy-round3.py", "logs/_lsy-round3.txt",
+         "logs/_lsy-close-control.py", "logs/_lsy-close-control.txt"]
+files = list(loose)
+for d in dirs:
+    if os.path.isdir(d):
+        for n in sorted(os.listdir(d)):
+            files.append(os.path.join(d, n))
+
+bad, n = [], 0
+for f in files:
+    n += 1
+    c = hits(f)
+    if c:
+        bad.append((f, c))
+
+print("扫描文件数 = %d" % n)
+print("含密钥的文件数 = %d" % len(bad))
+for f, c in bad:
+    print("  !! %s  命中 %d 次" % (f, c))
+print("结论: %s" % ("干净,可提交" if not bad else "**有泄漏,不得提交**"))

+ 1 - 0
logs/_lsy-lc/cancel-nonexistent.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"cancel_trace_no":null,"cancel_time":null,"trade_no":null,"pay_trace_no":null,"pay_time":null,"sign":null}

+ 1 - 0
logs/_lsy-lc/close-qr-1st.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"聚合码主动关单成功!","result_code":"PAY_SUCCESS","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","sign":"58b1cb3946a1dc8ef76182e462d74e0b"}

+ 1 - 0
logs/_lsy-lc/close-qr-2nd.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"该聚合码链接不存在或已过期!","result_code":"PAY_FAIL","mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"sign":null}

+ 1 - 0
logs/_lsy-lc/close-qr-nocodeurl.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"有必传字段未传!","result_code":"PAY_FAIL","mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"sign":null}

+ 1 - 0
logs/_lsy-lc/close-qr-nonexistent.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"该聚合码链接不存在或已过期!","result_code":"PAY_FAIL","mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"sign":null}

+ 1 - 0
logs/_lsy-lc/closeorder-h5-bytradeno.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"关单成功","result_code":"PAY_SUCCESS","mch_no":null,"close_trace_no":"cl162419724611","close_time":"20260928162419","trade_no":"200001070X4326092816241610002","pay_trace_no":"h5162416152af0","pay_time":"20260928162416","sign":"32fc02d5d72062382675c99bfc22002a"}

+ 1 - 0
logs/_lsy-lc/closeorder-qr-bytrace.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"mch_no":null,"close_trace_no":null,"close_time":null,"trade_no":null,"pay_trace_no":null,"pay_time":null,"sign":null}

+ 1 - 0
logs/_lsy-lc/create-h5pay.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"预支付成功","result_code":"PAY_SUCCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","pay_trace_no":"h5162416152af0","pay_time":"20260928162416","total_amount":"1","trade_no":"200001070X4326092816241610002","appId":null,"timeStamp":null,"nonceStr":null,"packages":null,"signType":null,"paySign":null,"ali_trade_no":null,"attach":null,"code_url":"https://yyfweb.postar.cn/postalpay/p/generateCode/cloud?threeOrderNo=200001070X4326092816241610002&agetId=FWH000012501&orderDate=20260928","sign":"f2fa0f2521ee3498cc250f30af324921","trade_state":null,"pay_amount":null,"coupon_amount":null,"receipt_amount":null,"discount_amount":null,"miniapp_data":null,"mem_data":null}

+ 1 - 0
logs/_lsy-lc/create-pay-badcode.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"未知的条码类型!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"pay_trace_no":null,"pay_time":null,"total_amount":null,"end_time":null,"trade_no":null,"td_trade_no":null,"qd_trade_no":null,"user_id":null,"sub_openid":null,"attach":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"invoice_entry":null,"async_payment_mode":null,"sign":null,"trade_state":null}

+ 1 - 0
logs/_lsy-lc/create-pay-barcode.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"支付失败,获取顾客账户信息失败,请顾客刷新付款码后重新收款,如再次收款失败,请联系管理员处理。[SOUNDWAVE_PARSER_FAIL]","result_code":"PAY_FAIL","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","pay_trace_no":"bc16241607d898","pay_time":"20260928162416","total_amount":"1","end_time":"20260928162417","trade_no":"200001070X3126092816241710003","td_trade_no":null,"qd_trade_no":"20260928MM6r8o25","user_id":null,"sub_openid":null,"attach":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"invoice_entry":null,"async_payment_mode":null,"sign":"3111f9ff303584e47a035584ead3fcc1","trade_state":null}

+ 1 - 0
logs/_lsy-lc/create-qrpay-dup.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"商户请求流水号已存在!","result_code":"PAY_FAIL","mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"pay_trace_no":null,"pay_time":null,"total_amount":null,"code_url":null,"sign":null}

+ 1 - 0
logs/_lsy-lc/create-qrpay-full.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"聚合码请求成功!","result_code":"PAY_SUCCESS","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","pay_trace_no":"qf16241670036a","pay_time":"20260928162416","total_amount":"2","code_url":"https://test.gdyfsk.com/yfpay/s/s-00005","sign":"3e31cb7f8b480946f40b810d72d5f81d"}

+ 1 - 0
logs/_lsy-lc/create-qrpay-min.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"聚合码请求成功!","result_code":"PAY_SUCCESS","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","pay_trace_no":"qr162415bb3c38","pay_time":"20260928162415","total_amount":"1","code_url":"https://test.gdyfsk.com/yfpay/s/s-00004","sign":"20bc562a8ecd10d763737ef3779bd456"}

+ 1 - 0
logs/_lsy-lc/query-h5-both-keys.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"用户支付中","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"58c34dd2fb824b1d8213c89d35cbfbf7","pay_trace_no":"h5162416152af0","pay_time":"20260928162416","total_amount":"1","end_time":null,"trade_no":"200001070X4326092816241610002","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":"USERPAYING","receipt_amount":"0","pay_amount":"1","coupon_amount":null,"discount_amount":"0","sign":"3e71e753d6e795905c20ca95a8e1e0cd"}

+ 1 - 0
logs/_lsy-lc/query-h5-bytrace.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"用户支付中","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"94b1bfe833864e1890bbcd2c3ec7dfd4","pay_trace_no":"h5162416152af0","pay_time":"20260928162416","total_amount":"1","end_time":null,"trade_no":"200001070X4326092816241610002","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":"USERPAYING","receipt_amount":"0","pay_amount":"1","coupon_amount":null,"discount_amount":"0","sign":"1acd6b2f519400c0b6fed789c294cd92"}

+ 1 - 0
logs/_lsy-lc/query-h5-bytradeno.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"用户支付中","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"3d73ef41a43c4b5aa08dc71df09326a5","pay_trace_no":"h5162416152af0","pay_time":"20260928162416","total_amount":"1","end_time":null,"trade_no":"200001070X4326092816241610002","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":"USERPAYING","receipt_amount":"0","pay_amount":"1","coupon_amount":null,"discount_amount":"0","sign":"5ee0ded241a12234ea2abc443778009f"}

+ 1 - 0
logs/_lsy-lc/query-h5-traceONLY.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"用户支付中","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"40e441a0bd924baabcb3dd63c4cd87b5","pay_trace_no":"h5162416152af0","pay_time":"20260928162416","total_amount":"1","end_time":null,"trade_no":"200001070X4326092816241610002","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":"USERPAYING","receipt_amount":"0","pay_amount":"1","coupon_amount":null,"discount_amount":"0","sign":"1188a114f0d0e9932562a39034280a6c"}

+ 1 - 0
logs/_lsy-lc/query-nokey.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"query_trace_no":null,"pay_trace_no":null,"pay_time":null,"total_amount":null,"end_time":null,"trade_no":null,"td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"a3cb8169d863e438486a6ea0ae72fea7"}

+ 1 - 0
logs/_lsy-lc/query-nonexistent.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"query_trace_no":null,"pay_trace_no":null,"pay_time":null,"total_amount":null,"end_time":null,"trade_no":null,"td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"a3cb8169d863e438486a6ea0ae72fea7"}

+ 1 - 0
logs/_lsy-lc/query-qrpay-afterclose.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"query_trace_no":null,"pay_trace_no":null,"pay_time":null,"total_amount":null,"end_time":null,"trade_no":null,"td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"a3cb8169d863e438486a6ea0ae72fea7"}

+ 1 - 0
logs/_lsy-lc/query-qrpay-unscanned.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"query_trace_no":null,"pay_trace_no":null,"pay_time":null,"total_amount":null,"end_time":null,"trade_no":null,"td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"a3cb8169d863e438486a6ea0ae72fea7"}

+ 1 - 0
logs/_lsy-lc/query-supply-bytrace.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"支付成功","result_code":"PAY_SUCCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":null,"mch_no":"Y52211090004","shop_no":null,"query_trace_no":"733e9b82428f4c9c8fff1b8fbda39cab","pay_trace_no":"sup162415362a15","pay_time":"20260928162415","total_amount":"1","end_time":"20260928162415","trade_no":"200001070J1226092816241610001","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":"SUCCESS","receipt_amount":"1","pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"fc37d3ae4afa60538f8dd39a508ea542"}

+ 1 - 0
logs/_lsy-lc/query-supply-bytradeno.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"支付成功","result_code":"PAY_SUCCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":null,"mch_no":"Y52211090004","shop_no":null,"query_trace_no":"76b788edea764042852aae39c914e623","pay_trace_no":"sup162415362a15","pay_time":"20260928162415","total_amount":"1","end_time":"20260928162415","trade_no":"200001070J1226092816241610001","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":"SUCCESS","receipt_amount":"1","pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"eb3ca4939f3a72f33cabe953ad0179bd"}

+ 1 - 0
logs/_lsy-lc/queryrefund-byno.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"查询失败:商户号[105000851370940]已注销或待审核","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":null,"mch_no":"Y52211090004","shop_no":null,"query_trace_no":"86d2a7b0913842b0b6934dbe1da5287f","refund_trace_no":"rf162420b076a3","refund_time":"20260928162420","total_amount":"1","refund_amount":"1","refund_settle":null,"trade_no":"200001070J1226092816241610001","refund_no":"092816242010004","td_trade_no":null,"qd_trade_no":"179058386000001","user_id":null,"attach":null,"undiscount_amount":null,"sign":"23354d1d8acde4e7482b9936001a8f93"}

+ 1 - 0
logs/_lsy-lc/queryrefund-bytrace.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"query_trace_no":null,"refund_trace_no":null,"refund_time":null,"total_amount":null,"refund_amount":null,"refund_settle":null,"trade_no":null,"refund_no":null,"td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"undiscount_amount":null,"sign":null}

+ 1 - 0
logs/_lsy-lc/refund-nonexistent.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"refund_trace_no":null,"pay_time":null,"refund_fee":null,"actual_refund_fee":null,"end_time":null,"refund_no":null,"qd_refund_no":null,"sign":null}

+ 1 - 0
logs/_lsy-lc/refund-overamount.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"退款失败:没有查询到符合条件的记录。","result_code":"PAY_FAIL","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":null,"mch_no":"Y52211090004","shop_no":null,"refund_trace_no":"rx162422b143d5","pay_time":"20260928162422","refund_fee":"999999","actual_refund_fee":null,"end_time":"20260928162423","refund_no":"092816242210005","qd_refund_no":null,"sign":"4a11a24bdfcdace4f5ff1dba38ad6fc5"}

+ 1 - 0
logs/_lsy-lc/refund-supply-bytrace.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"退款失败:没有查询到符合条件的记录。","result_code":"PAY_FAIL","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":null,"mch_no":"Y52211090004","shop_no":null,"refund_trace_no":"rf162420b076a3","pay_time":"20260928162420","refund_fee":"1","actual_refund_fee":null,"end_time":"20260928162422","refund_no":"092816242010004","qd_refund_no":null,"sign":"6dd2b1d6736d861339c367c096b15c44"}

+ 1 - 0
logs/_lsy-lc/refund-unpaid-qrpay.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"refund_trace_no":null,"pay_time":null,"refund_fee":null,"actual_refund_fee":null,"end_time":null,"refund_no":null,"qd_refund_no":null,"sign":null}

+ 1 - 0
logs/_lsy-lc/supply-1.bin

@@ -0,0 +1 @@
+{"code":200,"data":"200001070J1226092816241610001","message":"操作成功"}

+ 1 - 0
logs/_lsy-lc2/cancel-barcode-bytradeno.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"暂不支持撤销!","result_code":null,"cancel_trace_no":null,"cancel_time":null,"trade_no":null,"pay_trace_no":null,"pay_time":null,"sign":"f38faf1c35760e58792dc79d1827e2bb"}

+ 1 - 0
logs/_lsy-lc2/q-barcode-aftercancel.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"支付失败","result_code":"PAY_FAIL","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"eaace987b34f41e1998168971cd61a97","pay_trace_no":"bc16241607d898","pay_time":"20260928162416","total_amount":"1","end_time":"20260928162417","trade_no":"200001070X3126092816241710003","td_trade_no":null,"qd_trade_no":"20260928MM6r8o25","user_id":null,"attach":null,"trade_state":null,"receipt_amount":"1","pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"78b2cb961b3dbfe96056ac4f41126676"}

+ 1 - 0
logs/_lsy-lc2/qrefund-byno-again.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"查询失败:商户号[105000851370940]已注销或待审核","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":null,"mch_no":"Y52211090004","shop_no":null,"query_trace_no":"d8c3a4dde4264bb9aaf60fad59dbed7d","refund_trace_no":"rf162420b076a3","refund_time":"20260928162420","total_amount":"1","refund_amount":"1","refund_settle":null,"trade_no":"200001070J1226092816241610001","refund_no":"092816242010004","td_trade_no":null,"qd_trade_no":"179058386000001","user_id":null,"attach":null,"undiscount_amount":null,"sign":"f1c9cd630765d9b01826363eb8fd07a0"}

+ 1 - 0
logs/_lsy-lc2/qrefund-bytrace-fixed.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"查询失败:商户号[105000851370940]已注销或待审核","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":null,"mch_no":"Y52211090004","shop_no":null,"query_trace_no":"941e7b65f19a4e8dac2e8d090456f0be","refund_trace_no":"rf162420b076a3","refund_time":"20260928162420","total_amount":"1","refund_amount":"1","refund_settle":null,"trade_no":"200001070J1226092816241610001","refund_no":"092816242010004","td_trade_no":null,"qd_trade_no":"179058386000001","user_id":null,"attach":null,"undiscount_amount":null,"sign":"5f3283ffd0890826751541fa62f74675"}

+ 1 - 0
logs/_lsy-lc2/refund-closed-h5.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"refund_trace_no":null,"pay_time":null,"refund_fee":null,"actual_refund_fee":null,"end_time":null,"refund_no":null,"qd_refund_no":null,"sign":null}

+ 1 - 0
logs/_lsy-lc2/refund-tradeno-610001.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"退款失败:没有查询到符合条件的记录。","result_code":"PAY_FAIL","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":null,"mch_no":"Y52211090004","shop_no":null,"refund_trace_no":"rt162632d50747","pay_time":"20260928162632","refund_fee":"1","actual_refund_fee":null,"end_time":"20260928162633","refund_no":"092816263310008","qd_refund_no":null,"sign":"f26f1697cd5286f9c34346130c15355c"}

+ 1 - 0
logs/_lsy-lc2/supply-ch1.bin

@@ -0,0 +1 @@
+{"code":500,"data":{},"message":"商户未配置该通道"}

+ 1 - 0
logs/_lsy-lc2/supply-ch2.bin

@@ -0,0 +1 @@
+{"success":"error","msg":"result returns more than one elements; nested exception is javax.persistence.NonUniqueResultException: result returns more than one elements"}

+ 1 - 0
logs/_lsy-lc2/supply-ch4.bin

@@ -0,0 +1 @@
+{"code":500,"data":{},"message":"商户未配置该通道"}

+ 1 - 0
logs/_lsy-lc2/supply-ch50.bin

@@ -0,0 +1 @@
+{"code":200,"data":"200001070J1226092816253210006","message":"操作成功"}

+ 1 - 0
logs/_lsy-lc2/supply-ch6.bin

@@ -0,0 +1 @@
+{"code":500,"data":{},"message":"商户未配置该通道"}

+ 1 - 0
logs/_lsy-lc2/supply-flag2.bin

@@ -0,0 +1 @@
+{"code":200,"data":"200001070J1226092816263510009","message":"操作成功"}

+ 1 - 0
logs/_lsy-lc3/barcode-for-cancel.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"支付失败,获取顾客账户信息失败,请顾客刷新付款码后重新收款,如再次收款失败,请联系管理员处理。[SOUNDWAVE_PARSER_FAIL]","result_code":"PAY_FAIL","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","pay_trace_no":"bc2162917048b5d","pay_time":"20260928162917","total_amount":"1","end_time":"20260928162918","trade_no":"200001070X3126092816291710012","td_trade_no":null,"qd_trade_no":"20260928MM6sl2n9","user_id":null,"sub_openid":null,"attach":null,"receipt_amount":null,"pay_amount":null,"coupon_amount":null,"discount_amount":null,"invoice_entry":null,"async_payment_mode":null,"sign":"05f704eab5ab6372e3b38da1dc3b3e5d","trade_state":null}

+ 1 - 0
logs/_lsy-lc3/cancel-barcode-2nd.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"暂不支持撤销!","result_code":null,"cancel_trace_no":null,"cancel_time":null,"trade_no":null,"pay_trace_no":null,"pay_time":null,"sign":"f38faf1c35760e58792dc79d1827e2bb"}

+ 1 - 0
logs/_lsy-lc3/cancel-barcode-bytrace.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"暂不支持撤销!","result_code":null,"cancel_trace_no":null,"cancel_time":null,"trade_no":null,"pay_trace_no":null,"pay_time":null,"sign":"f38faf1c35760e58792dc79d1827e2bb"}

+ 1 - 0
logs/_lsy-lc3/co-barcode.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"关单失败:该订单交易失败,不支持关单","result_code":"PAY_FAIL","mch_no":null,"close_trace_no":"k4162916e34428","close_time":"20260928162916","trade_no":"200001070X3126092816241710003","pay_trace_no":"bc16241607d898","pay_time":"20260928162416","sign":"57132a5a66a1e713279b8a7bf4bda31b"}

+ 1 - 0
logs/_lsy-lc3/co-h5-1st.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"关单成功","result_code":"PAY_SUCCESS","mch_no":null,"close_trace_no":"k1162915e9062a","close_time":"20260928162915","trade_no":"200001070X4326092816291510011","pay_trace_no":"h5c162914735c13","pay_time":"20260928162914","sign":"c4ccf9c415aaa8ce2eb4e8acc8a0bb52"}

+ 1 - 0
logs/_lsy-lc3/co-h5-2nd-幂等.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"关单失败:该订单交易失败,不支持关单","result_code":"PAY_FAIL","mch_no":null,"close_trace_no":"k216291512dc75","close_time":"20260928162915","trade_no":"200001070X4326092816291510011","pay_trace_no":"h5c162914735c13","pay_time":"20260928162914","sign":"519105c54f6d3d2c2400f2d355c00fea"}

+ 1 - 0
logs/_lsy-lc3/co-nokey.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"mch_no":null,"close_trace_no":null,"close_time":null,"trade_no":null,"pay_trace_no":null,"pay_time":null,"sign":null}

+ 1 - 0
logs/_lsy-lc3/co-nonexistent.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"mch_no":null,"close_trace_no":null,"close_time":null,"trade_no":null,"pay_trace_no":null,"pay_time":null,"sign":null}

+ 1 - 0
logs/_lsy-lc3/co-oldh5-again.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"关单失败:该订单交易失败,不支持关单","result_code":"PAY_FAIL","mch_no":null,"close_trace_no":"k5162916bae82a","close_time":"20260928162916","trade_no":"200001070X4326092816241610002","pay_trace_no":"h5162416152af0","pay_time":"20260928162416","sign":"cb3d3bd6884817d5a484521713913e3c"}

+ 1 - 0
logs/_lsy-lc3/h5-for-close.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"预支付成功","result_code":"PAY_SUCCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","pay_trace_no":"h5c162914735c13","pay_time":"20260928162914","total_amount":"1","trade_no":"200001070X4326092816291510011","appId":null,"timeStamp":null,"nonceStr":null,"packages":null,"signType":null,"paySign":null,"ali_trade_no":null,"attach":null,"code_url":"https://yyfweb.postar.cn/postalpay/p/generateCode/cloud?threeOrderNo=200001070X4326092816291510011&agetId=FWH000012501&orderDate=20260928","sign":"555d66389a44700a00f1215e212dbf92","trade_state":null,"pay_amount":null,"coupon_amount":null,"receipt_amount":null,"discount_amount":null,"miniapp_data":null,"mem_data":null}

+ 1 - 0
logs/_lsy-lc3/q-ch50-bytradeno.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"系统异常","result_code":"PAY_IN_PROCESS","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":null,"mch_no":"Y52211090004","shop_no":null,"query_trace_no":"92ee930ce9264040bcd49ad33d2fe5eb","pay_trace_no":"s016253269843a","pay_time":"20260928162532","total_amount":"1","end_time":"20260928162532","trade_no":"200001070J1226092816253210006","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":"1","pay_amount":null,"coupon_amount":null,"discount_amount":null,"sign":"346d56e36955df5a56f78b88b79d5fd0"}

+ 1 - 0
logs/_lsy-lc3/q-h5-afterclose.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"支付失败","result_code":"PAY_FAIL","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"2825f31ce8134c6aa2a4591a6650839e","pay_trace_no":"h5c162914735c13","pay_time":"20260928162914","total_amount":"1","end_time":"20260928162915","trade_no":"200001070X4326092816291510011","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":"0","pay_amount":"1","coupon_amount":null,"discount_amount":"0","sign":"79d10051bd65c016963befe72c5a84ee"}

+ 1 - 0
logs/_lsy-lc3/q-oldh5-closed.bin

@@ -0,0 +1 @@
+{"return_code":"SUCCESS","return_msg":"支付失败","result_code":"PAY_FAIL","pay_type":"300","mch_name":"建行小店基本休息修改","shop_name":"","mch_no":"Y52211090004","shop_no":"","query_trace_no":"48decd2546104fd68bc30540af788149","pay_trace_no":"h5162416152af0","pay_time":"20260928162416","total_amount":"1","end_time":"20260928162420","trade_no":"200001070X4326092816241610002","td_trade_no":null,"qd_trade_no":null,"user_id":null,"attach":null,"trade_state":null,"receipt_amount":"0","pay_amount":"1","coupon_amount":null,"discount_amount":"0","sign":"f70a9a7969f9419d7ed4a6b404db3205"}

+ 1 - 0
logs/_lsy-lc3/refund-barcode-unpaid.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"refund_trace_no":null,"pay_time":null,"refund_fee":null,"actual_refund_fee":null,"end_time":null,"refund_no":null,"qd_refund_no":null,"sign":null}

+ 1 - 0
logs/_lsy-lc3/refund-bogus-tradeno.bin

@@ -0,0 +1 @@
+{"return_code":"FAIL","return_msg":"订单信息不存在!","result_code":null,"pay_type":null,"mch_name":null,"shop_name":null,"mch_no":null,"shop_no":null,"refund_trace_no":null,"pay_time":null,"refund_fee":null,"actual_refund_fee":null,"end_time":null,"refund_no":null,"qd_refund_no":null,"sign":null}

+ 419 - 0
logs/_lsy-lifecycle.py

@@ -0,0 +1,419 @@
+# -*- coding: utf-8 -*-
+"""乐收赢全链路联调台子:收单 / 查单 / 退款 / 退款查询 / 关单 / 撤销。
+
+设计要点
+  * 每一条出站都把**原始应答字节**落盘(logs/_lsy-lc/<tag>.bin),报告里只写结论。
+  * 每一条出站都做**入站验签**(null 不参与)—— 验签绝不跳过。
+  * 单条失败不中断:整轮跑完,末尾给汇总表。
+  * 控制台是 GBK ⇒ stdout 只打 ASCII,正文进 UTF-8 文件。
+  * 密钥只从 application.yml 读,只打长度。
+
+用法: python logs/_lsy-lifecycle.py [section ...]
+      section ∈ create, query, refund, close, cancel, supply, all(默认 all)
+"""
+import io
+import json
+import os
+import re
+import sys
+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-lc"
+OUT = "logs/_lsy-lifecycle.txt"
+
+buf = []
+rows = []
+
+
+def say(s=""):
+    buf.append(s)
+    print(s.encode("ascii", "replace").decode())
+
+
+def row(tag, path, verdict, detail=""):
+    rows.append((tag, path, verdict, detail))
+
+
+# ---------------------------------------------------------------- 配置
+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
+    var, default = m.group(1), m.group(2)
+    return os.environ.get(var) or default
+
+
+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, expect_sign=True):
+    """签名 → POST → 落盘 → 入站验签。返回 (http_code, dict|None, raw)。"""
+    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=30) 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))
+        row(tag, path, "TRANSPORT_ERR", repr(e)[:80])
+        return None, None, b""
+
+    io.open("%s/%s.bin" % (OUTDIR, tag), "wb").write(raw)
+    try:
+        resp = json.loads(raw.decode("utf-8"))
+    except Exception:  # noqa: BLE001
+        say("  !! 应答不是 JSON(%d B)" % len(raw))
+        row(tag, path, "NON_JSON", "%d B" % len(raw))
+        return code, None, raw
+
+    # 入站验签(null 不参与)
+    usign = resp.get("sign")
+    vs = "无 sign"
+    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"
+    elif expect_sign:
+        vs = "无sign(异常)"
+
+    rcode = resp.get("return_code")
+    rescode = resp.get("result_code")
+    say("  POST %-22s -> http %s  %4d B  return_code=%s result_code=%s  %s"
+        % (path, code, len(raw), rcode, rescode, vs))
+    say("       return_msg = %s" % resp.get("return_msg"))
+    row(tag, path, "%s/%s" % (rcode, rescode), "%s; %s" % (str(resp.get("return_msg"))[:40], vs))
+    return code, resp, raw
+
+
+def stamp():
+    return time.strftime("%Y%m%d%H%M%S")
+
+
+def uid(prefix):
+    return "%s%s%s" % (prefix, time.strftime("%H%M%S"), uuid.uuid4().hex[:6])
+
+
+STATE = {}          # 跨用例共享的句柄
+
+
+# ---------------------------------------------------------------- E 补录
+def sec_supply():
+    say("=" * 78)
+    say("【补录】/v3/createSupply —— 造一笔「已支付成功」的单,解锁退款腿")
+    say("=" * 78)
+    trace = uid("sup")
+    now = stamp()
+    settle = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 60))
+    _, resp, _ = call("/v3/createSupply", {
+        "channel_type": "37",          # 37=建行(本商户名就是「建行小店…」)
+        "pay_mode": "2",               # 2=扫码支付
+        "type": "300",                 # 300=支付宝
+        "function_flag": "1",          # 1=补单交易成功订单
+        "pay_time": now,
+        "settle_time": settle,
+        "pay_trace_no": trace,
+        "total_fee": "1",
+        "order_body": "联调补录",
+    }, "supply-1")
+    if resp:
+        say("       data(乐收赢订单号) = %s" % resp.get("data"))
+        STATE["sup_trace"] = trace
+        STATE["sup_time"] = now
+        STATE["sup_no"] = resp.get("data")
+    say()
+
+
+# ---------------------------------------------------------------- A 收单
+def sec_create():
+    say("=" * 78)
+    say("【收单】三条腿 + 参数变体")
+    say("=" * 78)
+
+    # A1 聚合码:最小参数集(= 生产 createQrOrder 发的集合)
+    t1, p1 = uid("qr"), stamp()
+    _, r1, _ = call("/v3/qrpay", {
+        "pay_trace_no": t1, "pay_time": p1, "total_amount": "1",
+        "time_expire": "15", "order_body": "联调-主扫-最小",
+    }, "create-qrpay-min")
+    if r1:
+        STATE["qr"] = {"trace": t1, "time": p1, "code_url": r1.get("code_url"), "resp": r1}
+    say()
+
+    # A2 聚合码:全参数
+    t2, p2 = uid("qf"), stamp()
+    _, r2, _ = call("/v3/qrpay", {
+        "pay_trace_no": t2, "pay_time": p2, "total_amount": "2",
+        "time_expire": "30", "order_body": "联调-主扫-全参",
+        "attach": "A2", "notify_url": "https://example.com/notify",
+        "front_url": "https://example.com/front", "auto_payment": "0",
+        "undiscount_amount": "1",
+    }, "create-qrpay-full")
+    if r2:
+        STATE["qr2"] = {"trace": t2, "time": p2, "code_url": r2.get("code_url")}
+    say()
+
+    # A3 同号重下(幂等/拒绝)
+    if STATE.get("qr"):
+        call("/v3/qrpay", {
+            "pay_trace_no": STATE["qr"]["trace"], "pay_time": STATE["qr"]["time"],
+            "total_amount": "1", "order_body": "联调-重复单号",
+        }, "create-qrpay-dup")
+    say()
+
+    # A4 H5
+    t4, p4 = uid("h5"), stamp()
+    _, r4, _ = call("/v3/h5pay", {
+        "pay_trace_no": t4, "pay_time": p4, "total_amount": "1",
+        "pay_type": "300", "order_body": "联调-H5",
+    }, "create-h5pay")
+    if r4:
+        STATE["h5"] = {"trace": t4, "time": p4, "trade_no": r4.get("trade_no"),
+                       "code_url": r4.get("code_url")}
+    say()
+
+    # A5 被扫(无真付款码,预期失败;仍记一条腿的读数)
+    t5, p5 = uid("bc"), stamp()
+    call("/v3/pay", {
+        "pay_trace_no": t5, "pay_time": p5, "total_amount": "1",
+        "auth_code": "284812345678901234", "pay_type": "300", "order_body": "联调-被扫",
+    }, "create-pay-barcode")
+    say()
+
+    # A6 被扫:未知条码类型(错误路径)
+    t6, p6 = uid("bx"), stamp()
+    call("/v3/pay", {
+        "pay_trace_no": t6, "pay_time": p6, "total_amount": "1",
+        "auth_code": "0" * 18, "pay_type": "000",
+    }, "create-pay-badcode")
+    say()
+
+
+# ---------------------------------------------------------------- B 查单
+def sec_query():
+    say("=" * 78)
+    say("【查单】/v3/queryorder —— 五条键路")
+    say("=" * 78)
+
+    h5 = STATE.get("h5")
+    if h5 and h5.get("trade_no"):
+        call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                                "trade_no": h5["trade_no"]}, "query-h5-bytradeno")
+        call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                                "pay_trace_no": h5["trace"], "pay_time": h5["time"]},
+             "query-h5-bytrace")
+        call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                                "pay_trace_no": h5["trace"]}, "query-h5-traceONLY")
+        call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                                "trade_no": h5["trade_no"], "pay_trace_no": h5["trace"],
+                                "pay_time": h5["time"]}, "query-h5-both-keys")
+    say()
+
+    qr = STATE.get("qr")
+    if qr:
+        call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                                "pay_trace_no": qr["trace"], "pay_time": qr["time"]},
+             "query-qrpay-unscanned")
+    say()
+
+    sup = STATE.get("sup_trace")
+    if sup:
+        call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                                "pay_trace_no": sup, "pay_time": STATE["sup_time"]},
+             "query-supply-bytrace")
+        if STATE.get("sup_no"):
+            call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                                    "trade_no": STATE["sup_no"]}, "query-supply-bytradeno")
+    say()
+
+    call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                            "pay_trace_no": "nosuchorder" + uuid.uuid4().hex[:8],
+                            "pay_time": stamp()}, "query-nonexistent")
+    say()
+
+    # 键缺失:两个键都不发(文档说二选一必传)
+    call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex}, "query-nokey")
+    say()
+
+
+# ---------------------------------------------------------------- D 退款
+def sec_refund():
+    say("=" * 78)
+    say("【退款】/v3/refundorder + /v3/queryrefund")
+    say("=" * 78)
+
+    # D1 对**未支付**的聚合码单退款(错误路径,看上游怎么拒)
+    qr = STATE.get("qr")
+    if qr:
+        call("/v3/refundorder", {
+            "refund_trace_no": uid("rf"), "refund_fee": "1",
+            "pay_time": stamp(), "pay_trace_no": qr["trace"],
+            "pay_trace_time": qr["time"],
+        }, "refund-unpaid-qrpay")
+    say()
+
+    # D2 对**补录成功**的单退款(正路)
+    sup = STATE.get("sup_trace")
+    if sup:
+        rt = uid("rf")
+        _, r, _ = call("/v3/refundorder", {
+            "refund_trace_no": rt, "refund_fee": "1", "pay_time": stamp(),
+            "pay_trace_no": sup, "pay_trace_time": STATE["sup_time"],
+        }, "refund-supply-bytrace")
+        STATE["refund"] = {"trace": rt, "resp": r}
+        if r and not r.get("refund_no") and r.get("refund_trace_no"):
+            STATE["refund"]["trace"] = r.get("refund_trace_no")
+    say()
+
+    # D3 退款查询
+    if STATE.get("refund"):
+        rf = STATE["refund"]
+        if rf["resp"] and rf["resp"].get("refund_no"):
+            call("/v3/queryrefund", {"query_trace_no": uuid.uuid4().hex,
+                                     "refund_no": rf["resp"]["refund_no"]}, "queryrefund-byno")
+        call("/v3/queryrefund", {"query_trace_no": uuid.uuid4().hex,
+                                 "refund_trace_no": rf["trace"],
+                                 "refund_time": stamp()}, "queryrefund-bytrace")
+    say()
+
+    # D4 超额退款
+    if sup:
+        call("/v3/refundorder", {
+            "refund_trace_no": uid("rx"), "refund_fee": "999999",
+            "pay_time": stamp(), "pay_trace_no": sup,
+            "pay_trace_time": STATE["sup_time"],
+        }, "refund-overamount")
+    say()
+
+    # D5 退款不存在的单
+    call("/v3/refundorder", {
+        "refund_trace_no": uid("rn"), "refund_fee": "1", "pay_time": stamp(),
+        "pay_trace_no": "nosuch" + uuid.uuid4().hex[:8], "pay_trace_time": stamp(),
+    }, "refund-nonexistent")
+    say()
+
+
+# ---------------------------------------------------------------- C 关单
+def sec_close():
+    say("=" * 78)
+    say("【关单】/v3/qrclose(聚合码,按 code_url)+ 幂等 + 错误路径")
+    say("=" * 78)
+
+    qr = STATE.get("qr")
+    if qr and qr.get("code_url"):
+        call("/v3/qrclose", {"code_url": qr["code_url"]}, "close-qr-1st")
+        call("/v3/qrclose", {"code_url": qr["code_url"]}, "close-qr-2nd")
+        call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                                "pay_trace_no": qr["trace"], "pay_time": qr["time"]},
+             "query-qrpay-afterclose")
+    say()
+
+    call("/v3/qrclose", {"code_url": "https://test.gdyfsk.com/yfpay/s/s-99999"},
+         "close-qr-nonexistent")
+    call("/v3/qrclose", {}, "close-qr-nocodeurl")
+    say()
+
+    # H5 / 聚合码的通用关单腿(我方**没接** /v3/closeorder,这里顺手量一下它认不认)
+    h5 = STATE.get("h5")
+    if h5 and h5.get("trade_no"):
+        call("/v3/closeorder", {
+            "close_trace_no": uid("cl"), "close_time": stamp(),
+            "trade_no": h5["trade_no"],
+        }, "closeorder-h5-bytradeno")
+    if STATE.get("qr"):
+        call("/v3/closeorder", {
+            "close_trace_no": uid("cl"), "close_time": stamp(),
+            "pay_trace_no": STATE["qr"]["trace"], "pay_time": STATE["qr"]["time"],
+        }, "closeorder-qr-bytrace")
+    say()
+
+
+# ---------------------------------------------------------------- F 撤销
+def sec_cancel():
+    say("=" * 78)
+    say("【撤销】/v3/cancelorder(被扫)")
+    say("=" * 78)
+    call("/v3/cancelorder", {
+        "cancel_trace_no": uid("cx"), "cancel_time": stamp(),
+        "pay_trace_no": "nosuch" + uuid.uuid4().hex[:8], "pay_time": stamp(),
+    }, "cancel-nonexistent")
+    say()
+
+
+SECTIONS = {
+    "supply": sec_supply, "create": sec_create, "query": sec_query,
+    "refund": sec_refund, "close": sec_close, "cancel": sec_cancel,
+}
+ORDER = ["supply", "create", "query", "close", "refund", "cancel"]
+
+
+def main():
+    want = sys.argv[1:] or ["all"]
+    todo = ORDER if "all" in want else [s for s in ORDER if s in want]
+    say("乐收赢全链路联调 —— 测试环境 %s / inst_no=%s / mch_no=%s / key=<len=%d>"
+        % (BASE, INST, MCH, len(KEY)))
+    say("时间 %s" % time.strftime("%Y-%m-%d %H:%M:%S"))
+    say()
+    for s in todo:
+        try:
+            SECTIONS[s]()
+        except Exception as e:  # noqa: BLE001
+            say("!! 段落 %s 抛异常: %r" % (s, e))
+            say()
+
+    say("=" * 78)
+    say("汇总(%d 条)" % len(rows))
+    say("=" * 78)
+    say("%-26s %-22s %-22s %s" % ("tag", "path", "return/result", "note"))
+    for t, p, v, d in rows:
+        say("%-26s %-22s %-22s %s" % (t, p, v, d))
+
+    io.open(OUT, "w", encoding="utf-8").write("\n".join(buf) + "\n")
+    print("report -> %s" % OUT)
+
+
+main()

+ 137 - 0
logs/_lsy-lifecycle.txt

@@ -0,0 +1,137 @@
+乐收赢全链路联调 —— 测试环境 https://test.gdyfsk.com/yfpay / inst_no=900112 / mch_no=Y52211090004 / key=<len=32>
+时间 2026-09-28 16:24:15
+
+==============================================================================
+【补录】/v3/createSupply —— 造一笔「已支付成功」的单,解锁退款腿
+==============================================================================
+  POST /v3/createSupply       -> http 200    76 B  return_code=None result_code=None  无sign(异常)
+       return_msg = None
+       data(乐收赢订单号) = 200001070J1226092816241610001
+
+==============================================================================
+【收单】三条腿 + 参数变体
+==============================================================================
+  POST /v3/qrpay              -> http 200   363 B  return_code=SUCCESS result_code=PAY_SUCCESS  验签OK(11)
+       return_msg = 聚合码请求成功!
+
+  POST /v3/qrpay              -> http 200   363 B  return_code=SUCCESS result_code=PAY_SUCCESS  验签OK(11)
+       return_msg = 聚合码请求成功!
+
+  POST /v3/qrpay              -> http 200   242 B  return_code=FAIL result_code=PAY_FAIL  无sign(异常)
+       return_msg = 商户请求流水号已存在!
+
+  POST /v3/h5pay              -> http 200   778 B  return_code=SUCCESS result_code=PAY_SUCCESS  验签OK(13)
+       return_msg = 预支付成功
+
+  POST /v3/pay                -> http 200   784 B  return_code=SUCCESS result_code=PAY_FAIL  验签OK(14)
+       return_msg = 支付失败,获取顾客账户信息失败,请顾客刷新付款码后重新收款,如再次收款失败,请联系管理员处理。[SOUNDWAVE_PARSER_FAIL]
+
+  POST /v3/pay                -> http 200   494 B  return_code=FAIL result_code=None  无sign(异常)
+       return_msg = 未知的条码类型!
+
+==============================================================================
+【查单】/v3/queryorder —— 五条键路
+==============================================================================
+  POST /v3/queryorder         -> http 200   607 B  return_code=SUCCESS result_code=PAY_IN_PROCESS  验签OK(17)
+       return_msg = 用户支付中
+  POST /v3/queryorder         -> http 200   607 B  return_code=SUCCESS result_code=PAY_IN_PROCESS  验签OK(17)
+       return_msg = 用户支付中
+  POST /v3/queryorder         -> http 200   607 B  return_code=SUCCESS result_code=PAY_IN_PROCESS  验签OK(17)
+       return_msg = 用户支付中
+  POST /v3/queryorder         -> http 200   607 B  return_code=SUCCESS result_code=PAY_IN_PROCESS  验签OK(17)
+       return_msg = 用户支付中
+
+  POST /v3/queryorder         -> http 200   481 B  return_code=FAIL result_code=None  验签OK(2)
+       return_msg = 订单信息不存在!
+
+  POST /v3/queryorder         -> http 200   617 B  return_code=SUCCESS result_code=PAY_SUCCESS  验签OK(14)
+       return_msg = 支付成功
+  POST /v3/queryorder         -> http 200   617 B  return_code=SUCCESS result_code=PAY_SUCCESS  验签OK(14)
+       return_msg = 支付成功
+
+  POST /v3/queryorder         -> http 200   481 B  return_code=FAIL result_code=None  验签OK(2)
+       return_msg = 订单信息不存在!
+
+  POST /v3/queryorder         -> http 200   481 B  return_code=FAIL result_code=None  验签OK(2)
+       return_msg = 订单信息不存在!
+
+==============================================================================
+【关单】/v3/qrclose(聚合码,按 code_url)+ 幂等 + 错误路径
+==============================================================================
+  POST /v3/qrclose            -> http 200   237 B  return_code=SUCCESS result_code=PAY_SUCCESS  验签OK(7)
+       return_msg = 聚合码主动关单成功!
+  POST /v3/qrclose            -> http 200   179 B  return_code=FAIL result_code=PAY_FAIL  无sign(异常)
+       return_msg = 该聚合码链接不存在或已过期!
+  POST /v3/queryorder         -> http 200   481 B  return_code=FAIL result_code=None  验签OK(2)
+       return_msg = 订单信息不存在!
+
+  POST /v3/qrclose            -> http 200   179 B  return_code=FAIL result_code=PAY_FAIL  无sign(异常)
+       return_msg = 该聚合码链接不存在或已过期!
+  POST /v3/qrclose            -> http 200   161 B  return_code=FAIL result_code=PAY_FAIL  无sign(异常)
+       return_msg = 有必传字段未传!
+
+  POST /v3/closeorder         -> http 200   304 B  return_code=SUCCESS result_code=PAY_SUCCESS  验签OK(8)
+       return_msg = 关单成功
+  POST /v3/closeorder         -> http 200   199 B  return_code=FAIL result_code=None  无sign(异常)
+       return_msg = 订单信息不存在!
+
+==============================================================================
+【退款】/v3/refundorder + /v3/queryrefund
+==============================================================================
+  POST /v3/refundorder        -> http 200   306 B  return_code=FAIL result_code=None  无sign(异常)
+       return_msg = 订单信息不存在!
+
+  POST /v3/refundorder        -> http 200   462 B  return_code=SUCCESS result_code=PAY_FAIL  验签OK(11)
+       return_msg = 退款失败:没有查询到符合条件的记录。
+
+  POST /v3/queryrefund        -> http 200   649 B  return_code=SUCCESS result_code=PAY_IN_PROCESS  验签OK(14)
+       return_msg = 查询失败:商户号[105000851370940]已注销或待审核
+  POST /v3/queryrefund        -> http 200   422 B  return_code=FAIL result_code=None  无sign(异常)
+       return_msg = 订单信息不存在!
+
+  POST /v3/refundorder        -> http 200   467 B  return_code=SUCCESS result_code=PAY_FAIL  验签OK(11)
+       return_msg = 退款失败:没有查询到符合条件的记录。
+
+  POST /v3/refundorder        -> http 200   306 B  return_code=FAIL result_code=None  无sign(异常)
+       return_msg = 订单信息不存在!
+
+==============================================================================
+【撤销】/v3/cancelorder(被扫)
+==============================================================================
+  POST /v3/cancelorder        -> http 200   187 B  return_code=FAIL result_code=None  无sign(异常)
+       return_msg = 订单信息不存在!
+
+==============================================================================
+汇总(30 条)
+==============================================================================
+tag                        path                   return/result          note
+supply-1                   /v3/createSupply       None/None              None; 无sign(异常)
+create-qrpay-min           /v3/qrpay              SUCCESS/PAY_SUCCESS    聚合码请求成功!; 验签OK(11)
+create-qrpay-full          /v3/qrpay              SUCCESS/PAY_SUCCESS    聚合码请求成功!; 验签OK(11)
+create-qrpay-dup           /v3/qrpay              FAIL/PAY_FAIL          商户请求流水号已存在!; 无sign(异常)
+create-h5pay               /v3/h5pay              SUCCESS/PAY_SUCCESS    预支付成功; 验签OK(13)
+create-pay-barcode         /v3/pay                SUCCESS/PAY_FAIL       支付失败,获取顾客账户信息失败,请顾客刷新付款码后重新收款,如再次收款失败,请联; 验签OK(14)
+create-pay-badcode         /v3/pay                FAIL/None              未知的条码类型!; 无sign(异常)
+query-h5-bytradeno         /v3/queryorder         SUCCESS/PAY_IN_PROCESS 用户支付中; 验签OK(17)
+query-h5-bytrace           /v3/queryorder         SUCCESS/PAY_IN_PROCESS 用户支付中; 验签OK(17)
+query-h5-traceONLY         /v3/queryorder         SUCCESS/PAY_IN_PROCESS 用户支付中; 验签OK(17)
+query-h5-both-keys         /v3/queryorder         SUCCESS/PAY_IN_PROCESS 用户支付中; 验签OK(17)
+query-qrpay-unscanned      /v3/queryorder         FAIL/None              订单信息不存在!; 验签OK(2)
+query-supply-bytrace       /v3/queryorder         SUCCESS/PAY_SUCCESS    支付成功; 验签OK(14)
+query-supply-bytradeno     /v3/queryorder         SUCCESS/PAY_SUCCESS    支付成功; 验签OK(14)
+query-nonexistent          /v3/queryorder         FAIL/None              订单信息不存在!; 验签OK(2)
+query-nokey                /v3/queryorder         FAIL/None              订单信息不存在!; 验签OK(2)
+close-qr-1st               /v3/qrclose            SUCCESS/PAY_SUCCESS    聚合码主动关单成功!; 验签OK(7)
+close-qr-2nd               /v3/qrclose            FAIL/PAY_FAIL          该聚合码链接不存在或已过期!; 无sign(异常)
+query-qrpay-afterclose     /v3/queryorder         FAIL/None              订单信息不存在!; 验签OK(2)
+close-qr-nonexistent       /v3/qrclose            FAIL/PAY_FAIL          该聚合码链接不存在或已过期!; 无sign(异常)
+close-qr-nocodeurl         /v3/qrclose            FAIL/PAY_FAIL          有必传字段未传!; 无sign(异常)
+closeorder-h5-bytradeno    /v3/closeorder         SUCCESS/PAY_SUCCESS    关单成功; 验签OK(8)
+closeorder-qr-bytrace      /v3/closeorder         FAIL/None              订单信息不存在!; 无sign(异常)
+refund-unpaid-qrpay        /v3/refundorder        FAIL/None              订单信息不存在!; 无sign(异常)
+refund-supply-bytrace      /v3/refundorder        SUCCESS/PAY_FAIL       退款失败:没有查询到符合条件的记录。; 验签OK(11)
+queryrefund-byno           /v3/queryrefund        SUCCESS/PAY_IN_PROCESS 查询失败:商户号[105000851370940]已注销或待审核; 验签OK(14)
+queryrefund-bytrace        /v3/queryrefund        FAIL/None              订单信息不存在!; 无sign(异常)
+refund-overamount          /v3/refundorder        SUCCESS/PAY_FAIL       退款失败:没有查询到符合条件的记录。; 验签OK(11)
+refund-nonexistent         /v3/refundorder        FAIL/None              订单信息不存在!; 无sign(异常)
+cancel-nonexistent         /v3/cancelorder        FAIL/None              订单信息不存在!; 无sign(异常)

+ 279 - 0
logs/_lsy-refund-unlock.py

@@ -0,0 +1,279 @@
+# -*- coding: utf-8 -*-
+"""第二轮:把「退款」这条腿打通 —— 换渠道补录 + 换寻址键退款。
+
+第一轮的读数(logs/_lsy-lifecycle.txt):
+  * 补录(channel_type=37 建行)→ queryorder 查到 PAY_SUCCESS ✔
+  * 但 /v3/refundorder → PAY_FAIL「退款失败:没有查询到符合条件的记录。」
+  * /v3/queryrefund 把那条退款记录查回来了,return_msg = 「查询失败:商户号[105000370940]已注销或待审核」
+    —— 那个商户号不是我们的(我们的是 Y52211090004),是**渠道商户号**。
+  ⇒ 假设 R1:退款失败是因为 channel_type=37 那条渠道在建行侧没开通/已注销。
+     若成立,换一个渠道(50 平安 / 6 官方直连 / 其它)补录后**退款应当能往下走**。
+  ⇒ 假设 R2:退款失败是**寻址键**的问题(pay_trace_no+pay_trace_time 这条腿在退款接口上不通)。
+     若成立,用 trade_no 寻址应当能往下走。
+  两个假设**各改一个变量**,互不干扰。
+
+另测:真实被扫失败单的 /v3/cancelorder(文档:「如果此订单用户支付失败,会将此订单关闭」)。
+"""
+import io
+import json
+import os
+import re
+import sys
+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-lc2"
+OUT = "logs/_lsy-refund-unlock.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)
+
+# 第一轮遗留的真实句柄(读自原始应答字节,不手抄)
+BARCODE_TRADE_NO = "200001070X3126092816241710003"   # create-pay-barcode,PAY_FAIL 的真被扫单
+SUP_TRACE = None
+SUP_TIME = None
+SUP_NO = None
+for ln in io.open("logs/_lsy-lifecycle.txt", encoding="utf-8").read().split("\n"):
+    if "data(乐收赢订单号)" in ln:
+        SUP_NO = ln.split("=")[-1].strip()
+say("── 前置:读第一轮的补录句柄 ──")
+say("  补录乐收赢订单号 = %s" % SUP_NO)
+say("  真实被扫失败单   = %s" % BARCODE_TRADE_NO)
+say()
+
+
+def call(path, params, tag, envelope="v3"):
+    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=30) 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", repr(e)[:60]))
+        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))
+        ok = md5((A2 + "&key=" + KEY).encode("utf-8")).hexdigest().lower() == usign
+        vs = "验签OK(%d)" % len(nn) if ok else "验签FAIL(%d)" % len(nn)
+    elif "sign" not in resp:
+        vs = "无sign键"
+
+    rc = resp.get("return_code")
+    ec = resp.get("result_code")
+    if envelope == "legacy":
+        rc, ec = resp.get("code"), resp.get("data")
+    say("  POST %-20s -> %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])
+
+
+# ────────────────────────────────────────────── 0) 验签:FAIL 应答的 sign 覆盖了什么
+say("=" * 78)
+say("【0】先钉一件事:queryorder 的 FAIL 应答 sign 覆盖哪几个字段")
+say("=" * 78)
+raw = io.open("logs/_lsy-lc/query-qrpay-unscanned.bin", "rb").read()
+resp = json.loads(raw.decode("utf-8"))
+nn = {k: v for k, v in resp.items() if k != "sign" and v is not None}
+say("  非 null 字段 = %s" % sorted(nn))
+A2 = "&".join("%s=%s" % (k, nn[k]) for k in sorted(nn))
+mine = md5((A2 + "&key=" + KEY).encode("utf-8")).hexdigest().lower()
+say("  重算 == 上游 : %s" % (mine == resp["sign"]))
+say("  ⇒ 这两条 FAIL 的 sign 是**常量**(换单号、换 query_trace_no 都不变)")
+say()
+
+# ────────────────────────────────────────────── 1) 换渠道补录
+say("=" * 78)
+say("【1】假设 R1:换 channel_type 补录(各一条),再试退款")
+say("=" * 78)
+CHANNELS = [("6", "官方直连"), ("1", "网商"), ("2", "富友"), ("50", "平安"), ("4", "银联")]
+made = []
+for ch, name in CHANNELS:
+    trace = uid("s%d" % len(made))
+    now = stamp()
+    settle = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 60))
+    r = call("/v3/createSupply", {
+        "channel_type": ch, "pay_mode": "2", "type": "300", "function_flag": "1",
+        "pay_time": now, "settle_time": settle, "pay_trace_no": trace,
+        "total_fee": "1", "order_body": "二轮补录ch%s" % ch,
+    }, "supply-ch%s" % ch, envelope="legacy")
+    if r and r.get("code") == 200 and r.get("data"):
+        made.append({"ch": ch, "name": name, "trace": trace, "time": now, "no": r["data"]})
+        say("       ch=%s(%s) 订单号 = %s" % (ch, name, r["data"]))
+    say()
+
+# ────────────────────────────────────────────── 2) 每笔补录:查得到吗
+say("=" * 78)
+say("【2】每笔补录用 queryorder 复核(必须 PAY_SUCCESS 才值得往下退)")
+say("=" * 78)
+good = []
+for m in made:
+    q = call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                                "pay_trace_no": m["trace"], "pay_time": m["time"]},
+             "q-supply-ch%s" % m["ch"])
+    if q and q.get("result_code") == "PAY_SUCCESS":
+        good.append(m)
+    say()
+say("  查到 PAY_SUCCESS 的:%s" % [m["ch"] for m in good])
+say()
+
+# ────────────────────────────────────────────── 3) 假设 R1:换渠道退款(键不变)
+say("=" * 78)
+say("【3】假设 R1:同一套寻址键(pay_trace_no+pay_trace_time),只换渠道")
+say("=" * 78)
+for m in made:
+    say("  ── ch=%s(%s) ──" % (m["ch"], m["name"]))
+    call("/v3/refundorder", {
+        "refund_trace_no": uid("r%d" % len(m["ch"])), "refund_fee": "1",
+        "pay_time": stamp(), "pay_trace_no": m["trace"], "pay_trace_time": m["time"],
+    }, "refund-ch%s-bytrace" % m["ch"])
+    say()
+
+# ────────────────────────────────────────────── 4) 假设 R2:换寻址键(trade_no)
+say("=" * 78)
+say("【4】假设 R2:换寻址键为 trade_no(含第一轮那笔建行补录)")
+say("=" * 78)
+targets = [{"ch": "37(一轮建行)", "no": SUP_NO}] + [{"ch": m["ch"], "no": m["no"]} for m in good]
+for t in targets:
+    if not t["no"]:
+        continue
+    say("  ── trade_no = %s  ch=%s ──" % (t["no"], t["ch"]))
+    call("/v3/refundorder", {
+        "refund_trace_no": uid("rt"), "refund_fee": "1",
+        "pay_time": stamp(), "trade_no": t["no"],
+    }, "refund-tradeno-%s" % t["no"][-6:])
+    say()
+
+# ────────────────────────────────────────────── 5) 退款查询:把 refund_time 用对
+say("=" * 78)
+say("【5】/v3/queryrefund 两条寻址路(第一轮 bytrace 挂在我把 refund_time 填错)")
+say("=" * 78)
+first = json.loads(io.open("logs/_lsy-lc/refund-supply-bytrace.bin", "rb").read().decode("utf-8"))
+say("  第一轮那条退款:refund_no=%s refund_trace_no=%s" % (first.get("refund_no"), first.get("refund_trace_no")))
+say("  它回显的 pay_time(=发起退款的时间)= %s  ← 这才是 refund_time 该填的值" % first.get("pay_time"))
+call("/v3/queryrefund", {"query_trace_no": uuid.uuid4().hex,
+                         "refund_trace_no": first["refund_trace_no"],
+                         "refund_time": first["pay_time"]}, "qrefund-bytrace-fixed")
+call("/v3/queryrefund", {"query_trace_no": uuid.uuid4().hex,
+                         "refund_no": first["refund_no"]}, "qrefund-byno-again")
+say()
+
+# ────────────────────────────────────────────── 6) 撤销:真实被扫失败单
+say("=" * 78)
+say("【6】/v3/cancelorder —— 拿一笔**真实的被扫 PAY_FAIL 单**(文档:支付失败会关单)")
+say("=" * 78)
+call("/v3/cancelorder", {"cancel_trace_no": uid("cx"), "cancel_time": stamp(),
+                         "trade_no": BARCODE_TRADE_NO}, "cancel-barcode-bytradeno")
+call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex,
+                        "trade_no": BARCODE_TRADE_NO}, "q-barcode-aftercancel")
+say()
+
+# ────────────────────────────────────────────── 7) 退款一笔「已被关单」的 H5 单
+say("=" * 78)
+say("【7】对**已关单**的 H5 单退款(第一轮 closeorder 已把它关了)")
+say("=" * 78)
+h5 = json.loads(io.open("logs/_lsy-lc/create-h5pay.bin", "rb").read().decode("utf-8"))
+say("  H5 单 trade_no = %s(已被 /v3/closeorder 关闭)" % h5.get("trade_no"))
+call("/v3/refundorder", {"refund_trace_no": uid("rc"), "refund_fee": "1",
+                         "pay_time": stamp(), "trade_no": h5.get("trade_no")},
+     "refund-closed-h5")
+say()
+
+# ────────────────────────────────────────────── 8) 补录退款(function_flag=2)
+say("=" * 78)
+say("【8】/v3/createSupply function_flag=2(补录退款订单)—— 直接造一条退款记录")
+say("=" * 78)
+if SUP_NO:
+    call("/v3/createSupply", {
+        "channel_type": "37", "pay_mode": "2", "type": "300", "function_flag": "2",
+        "pay_time": stamp(), "settle_time": time.strftime("%Y-%m-%d %H:%M:%S"),
+        "pay_trace_no": uid("sup2"), "total_fee": "1", "out_refund_no": SUP_NO,
+        "order_body": "二轮补录退款",
+    }, "supply-flag2", envelope="legacy")
+say()
+
+say("=" * 78)
+say("汇总(%d 条)" % len(rows))
+say("=" * 78)
+say("%-30s %-22s %-20s %s" % ("tag", "path", "rc/ec", "note"))
+for t, p, v, d in rows:
+    say("%-30s %-22s %-20s %s" % (t, p, v, d))
+
+io.open(OUT, "w", encoding="utf-8").write("\n".join(buf) + "\n")
+print("report -> %s" % OUT)

+ 99 - 0
logs/_lsy-refund-unlock.txt

@@ -0,0 +1,99 @@
+── 前置:读第一轮的补录句柄 ──
+  补录乐收赢订单号 = 200001070J1226092816241610001
+  真实被扫失败单   = 200001070X3126092816241710003
+
+==============================================================================
+【0】先钉一件事:queryorder 的 FAIL 应答 sign 覆盖哪几个字段
+==============================================================================
+  非 null 字段 = ['return_code', 'return_msg']
+  重算 == 上游 : True
+  ⇒ 这两条 FAIL 的 sign 是**常量**(换单号、换 query_trace_no 都不变)
+
+==============================================================================
+【1】假设 R1:换 channel_type 补录(各一条),再试退款
+==============================================================================
+  POST /v3/createSupply     ->   59 B  rc=500 ec={}  无sign键
+       msg = 商户未配置该通道
+
+  POST /v3/createSupply     ->   59 B  rc=500 ec={}  无sign键
+       msg = 商户未配置该通道
+
+  POST /v3/createSupply     ->  168 B  rc=None ec=None  无sign键
+       msg = None
+
+  POST /v3/createSupply     ->   76 B  rc=200 ec=200001070J1226092816253210006  无sign键
+       msg = 操作成功
+       ch=50(平安) 订单号 = 200001070J1226092816253210006
+
+  POST /v3/createSupply     ->   59 B  rc=500 ec={}  无sign键
+       msg = 商户未配置该通道
+
+==============================================================================
+【2】每笔补录用 queryorder 复核(必须 PAY_SUCCESS 才值得往下退)
+==============================================================================
+  !! 未完成 q-supply-ch50: TimeoutError('The read operation timed out')
+
+  查到 PAY_SUCCESS 的:[]
+
+==============================================================================
+【3】假设 R1:同一套寻址键(pay_trace_no+pay_trace_time),只换渠道
+==============================================================================
+  ── ch=50(平安) ──
+  !! 未完成 refund-ch50-bytrace: TimeoutError('The read operation timed out')
+
+==============================================================================
+【4】假设 R2:换寻址键为 trade_no(含第一轮那笔建行补录)
+==============================================================================
+  ── trade_no = 200001070J1226092816241610001  ch=37(一轮建行) ──
+  POST /v3/refundorder      ->  462 B  rc=SUCCESS ec=PAY_FAIL  验签OK(11)
+       msg = 退款失败:没有查询到符合条件的记录。
+
+==============================================================================
+【5】/v3/queryrefund 两条寻址路(第一轮 bytrace 挂在我把 refund_time 填错)
+==============================================================================
+  第一轮那条退款:refund_no=092816242010004 refund_trace_no=rf162420b076a3
+  它回显的 pay_time(=发起退款的时间)= 20260928162420  ← 这才是 refund_time 该填的值
+  POST /v3/queryrefund      ->  649 B  rc=SUCCESS ec=PAY_IN_PROCESS  验签OK(14)
+       msg = 查询失败:商户号[105000851370940]已注销或待审核
+  POST /v3/queryrefund      ->  649 B  rc=SUCCESS ec=PAY_IN_PROCESS  验签OK(14)
+       msg = 查询失败:商户号[105000851370940]已注销或待审核
+
+==============================================================================
+【6】/v3/cancelorder —— 拿一笔**真实的被扫 PAY_FAIL 单**(文档:支付失败会关单)
+==============================================================================
+  POST /v3/cancelorder      ->  214 B  rc=FAIL ec=None  验签OK(2)
+       msg = 暂不支持撤销!
+  POST /v3/queryorder       ->  618 B  rc=SUCCESS ec=PAY_FAIL  验签OK(16)
+       msg = 支付失败
+
+==============================================================================
+【7】对**已关单**的 H5 单退款(第一轮 closeorder 已把它关了)
+==============================================================================
+  H5 单 trade_no = 200001070X4326092816241610002(已被 /v3/closeorder 关闭)
+  POST /v3/refundorder      ->  306 B  rc=FAIL ec=None  sign=null
+       msg = 订单信息不存在!
+
+==============================================================================
+【8】/v3/createSupply function_flag=2(补录退款订单)—— 直接造一条退款记录
+==============================================================================
+  POST /v3/createSupply     ->   76 B  rc=200 ec=200001070J1226092816263510009  无sign键
+       msg = 操作成功
+
+==============================================================================
+汇总(14 条)
+==============================================================================
+tag                            path                   rc/ec                note
+supply-ch6                     /v3/createSupply       500/{}               商户未配置该通道; 无sign键
+supply-ch1                     /v3/createSupply       500/{}               商户未配置该通道; 无sign键
+supply-ch2                     /v3/createSupply       None/None            None; 无sign键
+supply-ch50                    /v3/createSupply       200/200001070J1226092816253210006 操作成功; 无sign键
+supply-ch4                     /v3/createSupply       500/{}               商户未配置该通道; 无sign键
+q-supply-ch50                  /v3/queryorder         TRANSPORT_ERR        TimeoutError('The read operation timed out')
+refund-ch50-bytrace            /v3/refundorder        TRANSPORT_ERR        TimeoutError('The read operation timed out')
+refund-tradeno-610001          /v3/refundorder        SUCCESS/PAY_FAIL     退款失败:没有查询到符合条件的记录。; 验签OK(11)
+qrefund-bytrace-fixed          /v3/queryrefund        SUCCESS/PAY_IN_PROCESS 查询失败:商户号[105000851370940]已注销或待审核; 验签OK(14)
+qrefund-byno-again             /v3/queryrefund        SUCCESS/PAY_IN_PROCESS 查询失败:商户号[105000851370940]已注销或待审核; 验签OK(14)
+cancel-barcode-bytradeno       /v3/cancelorder        FAIL/None            暂不支持撤销!; 验签OK(2)
+q-barcode-aftercancel          /v3/queryorder         SUCCESS/PAY_FAIL     支付失败; 验签OK(16)
+refund-closed-h5               /v3/refundorder        FAIL/None            订单信息不存在!; sign=null
+supply-flag2                   /v3/createSupply       200/200001070J1226092816263510009 操作成功; 无sign键

+ 225 - 0
logs/_lsy-round3.py

@@ -0,0 +1,225 @@
+# -*- 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)

+ 83 - 0
logs/_lsy-round3.txt

@@ -0,0 +1,83 @@
+句柄(均读自原始应答字节)
+  ch50(平安) 补录单 = 200001070J1226092816253210006
+  ch37(建行) 补录单 = 200001070J1226092816241610001
+  真实被扫失败单     = 200001070X3126092816241710003
+  已关单的 H5        = 200001070X4326092816241610002
+
+==============================================================================
+【1】平安(50)渠道的补录单 —— 查询(第二轮超时,重跑)
+==============================================================================
+  POST /v3/queryorder         ->  614 B  rc=SUCCESS ec=PAY_IN_PROCESS  验签OK(13)
+       msg = 系统异常
+
+【1b】对它退款(先 trade_no 寻址)
+  !! 未完成 refund-ch50-tradeno: TimeoutError('The read operation timed out')  ← 量具失效,不是上游回答
+
+==============================================================================
+【2】关单:/v3/closeorder 全路径(我方**未实现**这个端点)
+==============================================================================
+  POST /v3/h5pay              ->  779 B  rc=SUCCESS ec=PAY_SUCCESS  验签OK(13)
+       msg = 预支付成功
+  新 H5 trade_no = 200001070X4326092816291510011
+
+  POST /v3/closeorder         ->  305 B  rc=SUCCESS ec=PAY_SUCCESS  验签OK(8)
+       msg = 关单成功
+  POST /v3/closeorder         ->  344 B  rc=SUCCESS ec=PAY_FAIL  验签OK(8)
+       msg = 关单失败:该订单交易失败,不支持关单
+  POST /v3/queryorder         ->  603 B  rc=SUCCESS ec=PAY_FAIL  验签OK(17)
+       msg = 支付失败
+
+  POST /v3/closeorder         ->  199 B  rc=FAIL ec=None  sign=null
+       msg = 订单信息不存在!
+  POST /v3/closeorder         ->  343 B  rc=SUCCESS ec=PAY_FAIL  验签OK(8)
+       msg = 关单失败:该订单交易失败,不支持关单
+  POST /v3/closeorder         ->  343 B  rc=SUCCESS ec=PAY_FAIL  验签OK(8)
+       msg = 关单失败:该订单交易失败,不支持关单
+  POST /v3/closeorder         ->  199 B  rc=FAIL ec=None  sign=null
+       msg = 订单信息不存在!
+
+==============================================================================
+【3】已关单的 H5 在 queryorder 里是什么样
+==============================================================================
+  POST /v3/queryorder         ->  602 B  rc=SUCCESS ec=PAY_FAIL  验签OK(17)
+       msg = 支付失败
+
+==============================================================================
+【4】退款错误路径补齐
+==============================================================================
+  POST /v3/refundorder        ->  306 B  rc=FAIL ec=None  sign=null
+       msg = 订单信息不存在!
+  POST /v3/refundorder        ->  306 B  rc=FAIL ec=None  sign=null
+       msg = 订单信息不存在!
+
+==============================================================================
+【5】撤销:换一笔再试(判「暂不支持撤销」是这一笔的还是普遍的)
+==============================================================================
+  POST /v3/pay                ->  785 B  rc=SUCCESS ec=PAY_FAIL  验签OK(14)
+       msg = 支付失败,获取顾客账户信息失败,请顾客刷新付款码后重新收款,如再次收款失败,请联系管理员处理。[SOUNDWAVE_PARSER_FAIL]
+  新被扫单 trade_no = 200001070X3126092816291710012
+  POST /v3/cancelorder        ->  214 B  rc=FAIL ec=None  验签OK(2)
+       msg = 暂不支持撤销!
+  POST /v3/cancelorder        ->  214 B  rc=FAIL ec=None  验签OK(2)
+       msg = 暂不支持撤销!
+
+==============================================================================
+汇总(16 条)
+==============================================================================
+tag                          path                   rc/ec                note
+q-ch50-bytradeno             /v3/queryorder         SUCCESS/PAY_IN_PROCESS 系统异常; 验签OK(13)
+refund-ch50-tradeno          /v3/refundorder        TRANSPORT_ERR        量具失效
+h5-for-close                 /v3/h5pay              SUCCESS/PAY_SUCCESS  预支付成功; 验签OK(13)
+co-h5-1st                    /v3/closeorder         SUCCESS/PAY_SUCCESS  关单成功; 验签OK(8)
+co-h5-2nd-幂等                 /v3/closeorder         SUCCESS/PAY_FAIL     关单失败:该订单交易失败,不支持关单; 验签OK(8)
+q-h5-afterclose              /v3/queryorder         SUCCESS/PAY_FAIL     支付失败; 验签OK(17)
+co-nokey                     /v3/closeorder         FAIL/None            订单信息不存在!; sign=null
+co-barcode                   /v3/closeorder         SUCCESS/PAY_FAIL     关单失败:该订单交易失败,不支持关单; 验签OK(8)
+co-oldh5-again               /v3/closeorder         SUCCESS/PAY_FAIL     关单失败:该订单交易失败,不支持关单; 验签OK(8)
+co-nonexistent               /v3/closeorder         FAIL/None            订单信息不存在!; sign=null
+q-oldh5-closed               /v3/queryorder         SUCCESS/PAY_FAIL     支付失败; 验签OK(17)
+refund-barcode-unpaid        /v3/refundorder        FAIL/None            订单信息不存在!; sign=null
+refund-bogus-tradeno         /v3/refundorder        FAIL/None            订单信息不存在!; sign=null
+barcode-for-cancel           /v3/pay                SUCCESS/PAY_FAIL     支付失败,获取顾客账户信息失败,请顾客刷新付款码后重新收款,如再次收款失败,请联系管理员; 验签OK(14)
+cancel-barcode-2nd           /v3/cancelorder        FAIL/None            暂不支持撤销!; 验签OK(2)
+cancel-barcode-bytrace       /v3/cancelorder        FAIL/None            暂不支持撤销!; 验签OK(2)