_lsy-round3.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. # -*- coding: utf-8 -*-
  2. """第三轮:① 平安(50)渠道的退款解锁重试;② 关单腿铺开;③ 若干收尾读数。
  3. 第二轮遗留:
  4. * supply-ch50 建单成功(200001070J1226092816253210006)但**查询超时** —— 量具失效,
  5. 不是负读数,必须重跑。
  6. * /v3/cancelorder 对真实被扫失败单回「暂不支持撤销!」(sign 有效)—— 待确认是不是
  7. 只对这一笔/这一个渠道成立。
  8. """
  9. import io
  10. import json
  11. import os
  12. import re
  13. import time
  14. import uuid
  15. import urllib.error
  16. import urllib.request
  17. from hashlib import md5
  18. YML = "java/src/main/resources/application.yml"
  19. VAR = re.compile(r"^\$\{([A-Za-z0-9_]+):(.*)\}$")
  20. OUTDIR = "logs/_lsy-lc3"
  21. OUT = "logs/_lsy-round3.txt"
  22. buf = []
  23. rows = []
  24. def say(s=""):
  25. buf.append(s)
  26. print(s.encode("ascii", "replace").decode())
  27. def read_block(path, top):
  28. lines = io.open(path, encoding="utf-8", errors="replace").read().split("\n")
  29. start = None
  30. for i, ln in enumerate(lines):
  31. if re.match(r"^%s:\s*$" % re.escape(top), ln):
  32. start = i
  33. break
  34. if start is None:
  35. raise SystemExit("找不到顶层段 %s:" % top)
  36. out = {}
  37. for j in range(start + 1, len(lines)):
  38. ln = lines[j]
  39. if ln.strip() == "" or ln.lstrip().startswith("#"):
  40. continue
  41. if not ln.startswith((" ", "\t")):
  42. break
  43. m = re.match(r"^\s+([A-Za-z0-9_.\-]+):\s*(.*)$", ln)
  44. if m:
  45. out[m.group(1)] = re.split(r"\s+#", m.group(2), maxsplit=1)[0].strip().strip("'\"")
  46. return out
  47. def resolve(raw):
  48. m = VAR.match(raw)
  49. if not m:
  50. return raw
  51. return os.environ.get(m.group(1)) or m.group(2)
  52. cfg = read_block(YML, "leshouying")
  53. BASE = resolve(cfg["base-url"]).rstrip("/")
  54. INST = resolve(cfg["inst-no"])
  55. KEY = resolve(cfg["key"])
  56. MCH = "Y52211090004"
  57. if not os.path.isdir(OUTDIR):
  58. os.makedirs(OUTDIR)
  59. def call(path, params, tag, envelope="v3", timeout=60):
  60. p = dict(params)
  61. p.setdefault("inst_no", INST)
  62. p.setdefault("mch_no", MCH)
  63. A = "&".join("%s=%s" % (k, p[k]) for k in sorted(p) if p[k] is not None)
  64. p["sign"] = md5((A + "&key=" + KEY).encode("utf-8")).hexdigest().lower()
  65. body = json.dumps(p, ensure_ascii=False).encode("utf-8")
  66. req = urllib.request.Request(BASE + path, data=body,
  67. headers={"Content-Type": "application/json;charset=utf-8"},
  68. method="POST")
  69. try:
  70. with urllib.request.urlopen(req, timeout=timeout) as r:
  71. code, raw = r.status, r.read()
  72. except urllib.error.HTTPError as e:
  73. code, raw = e.code, e.read()
  74. except Exception as e: # noqa: BLE001
  75. say(" !! 未完成 %s: %r ← 量具失效,不是上游回答" % (tag, e))
  76. rows.append((tag, path, "TRANSPORT_ERR", "量具失效"))
  77. return None
  78. io.open("%s/%s.bin" % (OUTDIR, tag), "wb").write(raw)
  79. try:
  80. resp = json.loads(raw.decode("utf-8"))
  81. except Exception: # noqa: BLE001
  82. rows.append((tag, path, "NON_JSON", "%d B" % len(raw)))
  83. return None
  84. usign = resp.get("sign")
  85. vs = "sign=null"
  86. if usign:
  87. nn = {k: v for k, v in resp.items() if k != "sign" and v is not None}
  88. A2 = "&".join("%s=%s" % (k, nn[k]) for k in sorted(nn))
  89. vs = ("验签OK(%d)" % len(nn)
  90. if md5((A2 + "&key=" + KEY).encode("utf-8")).hexdigest().lower() == usign
  91. else "验签FAIL(%d)" % len(nn))
  92. elif "sign" not in resp:
  93. vs = "无sign键"
  94. rc, ec = resp.get("return_code"), resp.get("result_code")
  95. if envelope == "legacy":
  96. rc, ec = resp.get("code"), resp.get("data")
  97. say(" POST %-22s -> %4d B rc=%s ec=%s %s" % (path, len(raw), rc, ec, vs))
  98. say(" msg = %s" % (resp.get("return_msg") or resp.get("message")))
  99. rows.append((tag, path, "%s/%s" % (rc, ec),
  100. "%s; %s" % (str(resp.get("return_msg") or resp.get("message"))[:44], vs)))
  101. return resp
  102. def stamp():
  103. return time.strftime("%Y%m%d%H%M%S")
  104. def uid(p):
  105. return "%s%s%s" % (p, time.strftime("%H%M%S"), uuid.uuid4().hex[:6])
  106. # 从第二轮原始字节里读句柄,不手抄
  107. ch50 = json.loads(io.open("logs/_lsy-lc2/supply-ch50.bin", "rb").read().decode("utf-8"))
  108. CH50_NO = ch50["data"]
  109. ch37_no = "200001070J1226092816241610001"
  110. BARCODE = "200001070X3126092816241710003"
  111. H5_CLOSED = "200001070X4326092816241610002"
  112. say("句柄(均读自原始应答字节)")
  113. say(" ch50(平安) 补录单 = %s" % CH50_NO)
  114. say(" ch37(建行) 补录单 = %s" % ch37_no)
  115. say(" 真实被扫失败单 = %s" % BARCODE)
  116. say(" 已关单的 H5 = %s" % H5_CLOSED)
  117. say()
  118. # ─────────────────────────────── 1) 平安渠道:查询 + 退款
  119. say("=" * 78)
  120. say("【1】平安(50)渠道的补录单 —— 查询(第二轮超时,重跑)")
  121. say("=" * 78)
  122. q = call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex, "trade_no": CH50_NO},
  123. "q-ch50-bytradeno")
  124. say()
  125. say("【1b】对它退款(先 trade_no 寻址)")
  126. call("/v3/refundorder", {"refund_trace_no": uid("p5"), "refund_fee": "1",
  127. "pay_time": stamp(), "trade_no": CH50_NO}, "refund-ch50-tradeno")
  128. say()
  129. # ─────────────────────────────── 2) 关单腿铺开
  130. say("=" * 78)
  131. say("【2】关单:/v3/closeorder 全路径(我方**未实现**这个端点)")
  132. say("=" * 78)
  133. t, pt = uid("h5c"), stamp()
  134. h5 = call("/v3/h5pay", {"pay_trace_no": t, "pay_time": pt, "total_amount": "1",
  135. "pay_type": "300", "order_body": "三轮-关单用H5"}, "h5-for-close")
  136. NEW_H5 = h5.get("trade_no") if h5 else None
  137. say(" 新 H5 trade_no = %s" % NEW_H5)
  138. say()
  139. if NEW_H5:
  140. call("/v3/closeorder", {"close_trace_no": uid("k1"), "close_time": stamp(),
  141. "trade_no": NEW_H5}, "co-h5-1st")
  142. call("/v3/closeorder", {"close_trace_no": uid("k2"), "close_time": stamp(),
  143. "trade_no": NEW_H5}, "co-h5-2nd-幂等")
  144. call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex, "trade_no": NEW_H5},
  145. "q-h5-afterclose")
  146. say()
  147. # 关单:两个键都不发
  148. call("/v3/closeorder", {"close_trace_no": uid("k3"), "close_time": stamp()}, "co-nokey")
  149. # 关单:被扫单(文档说 closeorder 只用于二维码/JSAPI)
  150. call("/v3/closeorder", {"close_trace_no": uid("k4"), "close_time": stamp(),
  151. "trade_no": BARCODE}, "co-barcode")
  152. # 关单:已关过的老 H5
  153. call("/v3/closeorder", {"close_trace_no": uid("k5"), "close_time": stamp(),
  154. "trade_no": H5_CLOSED}, "co-oldh5-again")
  155. # 关单:不存在的单
  156. call("/v3/closeorder", {"close_trace_no": uid("k6"), "close_time": stamp(),
  157. "trade_no": "200001070X9999999999999999999"}, "co-nonexistent")
  158. say()
  159. # ─────────────────────────────── 3) 已关单的 H5 还能查到吗
  160. say("=" * 78)
  161. say("【3】已关单的 H5 在 queryorder 里是什么样")
  162. say("=" * 78)
  163. call("/v3/queryorder", {"query_trace_no": uuid.uuid4().hex, "trade_no": H5_CLOSED}, "q-oldh5-closed")
  164. say()
  165. # ─────────────────────────────── 4) 退款:真实被扫失败单 / 不存在单
  166. say("=" * 78)
  167. say("【4】退款错误路径补齐")
  168. say("=" * 78)
  169. call("/v3/refundorder", {"refund_trace_no": uid("rb"), "refund_fee": "1",
  170. "pay_time": stamp(), "trade_no": BARCODE}, "refund-barcode-unpaid")
  171. call("/v3/refundorder", {"refund_trace_no": uid("rz"), "refund_fee": "1",
  172. "pay_time": stamp(), "trade_no": "200001070X9999999999999999999"},
  173. "refund-bogus-tradeno")
  174. say()
  175. # ─────────────────────────────── 5) 撤销:再试一笔,确认「暂不支持」是不是普遍
  176. say("=" * 78)
  177. say("【5】撤销:换一笔再试(判「暂不支持撤销」是这一笔的还是普遍的)")
  178. say("=" * 78)
  179. t2, p2 = uid("bc2"), stamp()
  180. bc = call("/v3/pay", {"pay_trace_no": t2, "pay_time": p2, "total_amount": "1",
  181. "auth_code": "284812345678901234", "pay_type": "300",
  182. "order_body": "三轮-撤销用被扫"}, "barcode-for-cancel")
  183. if bc and bc.get("trade_no"):
  184. say(" 新被扫单 trade_no = %s" % bc["trade_no"])
  185. call("/v3/cancelorder", {"cancel_trace_no": uid("c2"), "cancel_time": stamp(),
  186. "trade_no": bc["trade_no"]}, "cancel-barcode-2nd")
  187. call("/v3/cancelorder", {"cancel_trace_no": uid("c3"), "cancel_time": stamp(),
  188. "pay_trace_no": t2, "pay_time": p2}, "cancel-barcode-bytrace")
  189. say()
  190. say("=" * 78)
  191. say("汇总(%d 条)" % len(rows))
  192. say("=" * 78)
  193. say("%-28s %-22s %-20s %s" % ("tag", "path", "rc/ec", "note"))
  194. for t_, p_, v, d in rows:
  195. say("%-28s %-22s %-20s %s" % (t_, p_, v, d))
  196. io.open(OUT, "w", encoding="utf-8").write("\n".join(buf) + "\n")
  197. print("report -> %s" % OUT)