_lsy-query-by-traceno-probe.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. # -*- coding: utf-8 -*-
  2. """判决探针:拿一笔**已知可查**的单,改用 `pay_trace_no + pay_time` 分支再查一次。
  3. 要分的两个假设(此前无法分辨):
  4. H1 `queryorder` 按 `pay_trace_no+pay_time` 能查到单,只是**未扫的聚合码单**查不到
  5. ⇒ 「聚合码未扫 = 不在查询域」成立,属上游语义问题。
  6. H2 `pay_trace_no+pay_time` 这条分支**根本不通**(至少在本测试环境/本商户上)
  7. ⇒ 原结论是用错键造出来的,对账腿对**所有**走这条分支的单都瞎,性质完全不同。
  8. 量具:用 H5 那笔单(`logs/_lsy-h5pay-resp.bin`)—— 它的 trade_no 分支**实测查得到**
  9. (`logs/_lsy-query-by-tradeno-resp.bin`,614 B / PAY_IN_PROCESS)。同一笔单换个键:
  10. 查得到 ⇒ H1;还是「订单信息不存在」⇒ H2。
  11. ★ 只读查询。键值一律从原始应答字节里读,不手抄。
  12. ★ 密钥只从 application.yml 读、只打长度。
  13. """
  14. import json
  15. import os
  16. import re
  17. import sys
  18. import uuid
  19. import urllib.request
  20. import urllib.error
  21. from hashlib import md5
  22. YML = "java/src/main/resources/application.yml"
  23. VAR = re.compile(r"^\$\{([A-Za-z0-9_]+):(.*)\}$")
  24. SRC_BIN = "logs/_lsy-h5pay-resp.bin"
  25. CTRL_BIN = "logs/_lsy-query-by-tradeno-resp.bin"
  26. OUT = "logs/_lsy-query-by-traceno-probe.txt"
  27. buf = []
  28. def say(s=""):
  29. buf.append(s)
  30. print(s.encode("ascii", "replace").decode())
  31. def read_block(path, top):
  32. lines = open(path, encoding="utf-8", errors="replace").read().split("\n")
  33. start = None
  34. for i, ln in enumerate(lines):
  35. if re.match(r"^%s:\s*$" % re.escape(top), ln):
  36. start = i
  37. break
  38. if start is None:
  39. raise SystemExit("找不到顶层段 %s:" % top)
  40. out = {}
  41. for j in range(start + 1, len(lines)):
  42. ln = lines[j]
  43. if ln.strip() == "" or ln.lstrip().startswith("#"):
  44. continue
  45. if not ln.startswith((" ", "\t")):
  46. break
  47. m = re.match(r"^\s+([A-Za-z0-9_.\-]+):\s*(.*)$", ln)
  48. if m:
  49. v = re.split(r"\s+#", m.group(2), maxsplit=1)[0].strip().strip("'\"")
  50. out[m.group(1)] = v
  51. return out
  52. def resolve(raw):
  53. m = VAR.match(raw)
  54. if not m:
  55. return raw, "application.yml 字面量"
  56. var, default = m.group(1), m.group(2)
  57. if os.environ.get(var):
  58. return os.environ[var], "环境变量 %s" % var
  59. return default, "application.yml 默认值(键 %s)" % var
  60. cfg = read_block(YML, "leshouying")
  61. base_url = resolve(cfg["base-url"])[0]
  62. inst_no = resolve(cfg["inst-no"])[0]
  63. key = resolve(cfg["key"])[0]
  64. prev = json.loads(open(SRC_BIN, "rb").read().decode("utf-8"))
  65. ctrl = json.loads(open(CTRL_BIN, "rb").read().decode("utf-8"))
  66. trace = prev.get("pay_trace_no")
  67. ptime = prev.get("pay_time")
  68. if not (trace and ptime):
  69. raise SystemExit("H5 应答里缺 pay_trace_no/pay_time ⇒ 前提不成立: %s" % sorted(prev))
  70. say("── 先摆正对照:同一笔单,trade_no 分支的既有读数(读自原始应答字节)──")
  71. say(" trade_no = %s" % prev.get("trade_no"))
  72. say(" pay_trace_no = %s" % trace)
  73. say(" pay_time = %s" % ptime)
  74. say(" 正对照读数 = return_code=%s / return_msg=%s / result_code=%s"
  75. % (ctrl.get("return_code"), ctrl.get("return_msg"), ctrl.get("result_code")))
  76. say(" 正对照必须是「查得到」—— 否则本探针的对照组不成立")
  77. if ctrl.get("result_code") is None:
  78. raise SystemExit("正对照本身就没查到 ⇒ 换个对照组,别用这笔")
  79. say()
  80. payload = {
  81. "inst_no": inst_no,
  82. "mch_no": prev.get("mch_no"),
  83. "query_trace_no": uuid.uuid4().hex,
  84. # ↓ 走 LsyQueryService 的**无 trade_no 分支**(故意不发 trade_no)
  85. "pay_trace_no": trace,
  86. "pay_time": ptime,
  87. }
  88. A = "&".join("%s=%s" % (k, payload[k]) for k in sorted(payload))
  89. sign = md5((A + "&key=" + key).encode("utf-8")).hexdigest().lower()
  90. payload["sign"] = sign
  91. say("── 将发送(不含密钥;sign 打码)──")
  92. shown = dict(payload)
  93. shown["sign"] = "<len=%d>" % len(sign)
  94. say(json.dumps(shown, ensure_ascii=False, indent=2))
  95. say(" 注意:**没有** trade_no 这个键 ⇒ 走的正是主扫单走的那条分支")
  96. say()
  97. url = base_url.rstrip("/") + "/v3/queryorder"
  98. body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
  99. say("── 出站 ──")
  100. say(" POST %s" % url)
  101. say(" bytes = %d" % len(body))
  102. say()
  103. req = urllib.request.Request(
  104. url, data=body,
  105. headers={"Content-Type": "application/json;charset=utf-8"},
  106. method="POST",
  107. )
  108. try:
  109. with urllib.request.urlopen(req, timeout=30) as r:
  110. code, raw = r.status, r.read()
  111. except urllib.error.HTTPError as e:
  112. code, raw = e.code, e.read()
  113. except Exception as e:
  114. say("!! 请求未完成: %r" % (e,))
  115. open(OUT, "w", encoding="utf-8").write("\n".join(buf))
  116. sys.exit(1)
  117. open("logs/_lsy-query-by-traceno-resp.bin", "wb").write(raw)
  118. resp = json.loads(raw.decode("utf-8"))
  119. say("── 上游应答 ──")
  120. say(" http_code = %s, bytes = %d" % (code, len(raw)))
  121. for k in sorted(resp):
  122. v = resp[k]
  123. say(" %-16s = %s" % (k, "null" if v is None else repr(v)))
  124. say()
  125. say("── 判决 ──")
  126. if resp.get("result_code") is not None:
  127. say(" 查到了 ⇒ H1:pay_trace_no+pay_time 分支**通**,")
  128. say(" 问题只出在「未扫的聚合码单」这个状态上。")
  129. else:
  130. say(" 没查到 ⇒ H2:这条分支**不通**(本测试环境/本商户),")
  131. say(" 原结论「聚合码未扫查不到」是用错键造出来的,对账腿对走这条分支的单**全瞎**。")
  132. say()
  133. usign = resp.get("sign")
  134. if usign:
  135. nonnull = {k: v for k, v in resp.items() if k != "sign" and v is not None}
  136. A2 = "&".join("%s=%s" % (k, nonnull[k]) for k in sorted(nonnull))
  137. mine = md5((A2 + "&key=" + key).encode("utf-8")).hexdigest().lower()
  138. say("── 入站验签(null 不参与)──")
  139. say(" 参与签名字段数 = %d" % len(nonnull))
  140. say(" 重算 == 上游 sign : %s" % (mine == usign))
  141. say()
  142. open(OUT, "w", encoding="utf-8").write("\n".join(buf))