_lsy-refunds-e2e.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. # -*- coding: utf-8 -*-
  2. """端到端:拿真令牌打**新端点**,看订单抽屉那两块(退款单 / 回调轨迹)到底收到什么。
  3. 与 `_lsy-order-refunds-probe.py` 的分工(别把两者混为一谈):
  4. * 那个探针直连库,跑「同一条 SQL」,**证明不了端点** —— 它自己 docstring 里写着这句。
  5. * 这个脚本走 HTTP,过 Spring Security / @PreAuthorize / Jackson / 租户拦截器,
  6. 拿到的是**页面真正会收到的那份 JSON**。它才是「抽屉里有没有东西」的证据。
  7. 只读:两个 GET,不写任何东西。
  8. 凭据纪律:只打令牌**长度**;响应正文落 UTF-8 文件,stdout 只打 ASCII。
  9. 控制台 GBK ⇒ 中文一律进文件。
  10. """
  11. import io
  12. import json
  13. import os
  14. import sys
  15. import urllib.error
  16. import urllib.request
  17. ROOT = r"D:\project2\payment-platform"
  18. OUT = os.path.join(ROOT, "logs", "_lsy-refunds-e2e.txt")
  19. BASE = os.environ.get("LSY_ADMIN_BASE", "http://localhost:8001/api/v1")
  20. ORDER = sys.argv[1] if len(sys.argv) > 1 else "BC1790649777"
  21. buf = []
  22. def say(s=""):
  23. buf.append(str(s))
  24. print(str(s).encode("ascii", "replace").decode())
  25. def req(method, path, token=None, body=None):
  26. """返回 (http_status, 解析后的 JSON 或原文)。业务错误也照样返回,由调用方判。"""
  27. data = json.dumps(body).encode("utf-8") if body is not None else None
  28. r = urllib.request.Request(BASE + path, data=data, method=method)
  29. r.add_header("Content-Type", "application/json")
  30. if token:
  31. r.add_header("Authorization", "Bearer " + token)
  32. try:
  33. with urllib.request.urlopen(r, timeout=30) as resp:
  34. raw = resp.read()
  35. return resp.status, json.loads(raw.decode("utf-8"))
  36. except urllib.error.HTTPError as e:
  37. raw = e.read()
  38. try:
  39. return e.code, json.loads(raw.decode("utf-8"))
  40. except Exception:
  41. return e.code, raw.decode("utf-8", "replace")
  42. # ---- 1. 登录取令牌 -------------------------------------------------------
  43. say("=" * 74)
  44. say(" 1. 取管理端令牌")
  45. say("=" * 74)
  46. st, cap = req("GET", "/system/auth/captcha/get")
  47. say(" captcha/get -> HTTP %s enable=%s" % (st, (cap.get("data") or {}).get("enable")))
  48. token = None
  49. st, users = req("GET", "/system/auth/auto-login/users")
  50. say(" auto-login/users -> HTTP %s" % st)
  51. lst = (users.get("data") if isinstance(users, dict) else None) or []
  52. say(" 免登录可选用户 %d 个" % len(lst))
  53. if lst:
  54. uid = lst[0].get("id")
  55. uname = lst[0].get("username")
  56. say(" 取第一个:username=%s id=%s" % (uname, uid))
  57. st, t = req("POST", "/system/auth/auto-login/token?user_id=%s" % uid)
  58. say(" auto-login/token -> HTTP %s" % st)
  59. tok = ((t.get("data") or {}) if isinstance(t, dict) else {}).get("token")
  60. if tok:
  61. st, res = req("POST", "/system/auth/auto-login?token=%s" % tok)
  62. say(" auto-login -> HTTP %s" % st)
  63. token = ((res.get("data") or {}) if isinstance(res, dict) else {}).get("access_token")
  64. if token:
  65. say(" 令牌已取得:len=%d (值不打印)" % len(token))
  66. else:
  67. say(" !没能取得令牌,下面两个端点会 401 —— 那就是本脚本的结论,不要当成端点坏了")
  68. # ---- 2. 新端点 /refunds --------------------------------------------------
  69. say("")
  70. say("=" * 74)
  71. say(" 2. GET /payment/collection/order/%s/refunds (本次新加的端点)" % ORDER)
  72. say("=" * 74)
  73. st, refunds = req("GET", "/payment/collection/order/%s/refunds" % ORDER, token)
  74. say(" HTTP %s" % st)
  75. say(json.dumps(refunds, ensure_ascii=False, indent=2))
  76. rows = ((refunds.get("data") if isinstance(refunds, dict) else None) or [])
  77. say(" -> %d 行" % len(rows))
  78. for i, r in enumerate(rows):
  79. say(" [%d] keys=%s" % (i, sorted(r.keys())))
  80. # 前端「退款单」那一节的 5 列 + 状态映射要读的键,逐个点名
  81. for k in ("out_refund_no", "refund_fee", "status", "refund_no", "finish_time"):
  82. say(" %-14s = %r" % (k, r.get(k, "<缺失>")))
  83. # ---- 3. 回调轨迹 /notify-logs -------------------------------------------
  84. say("")
  85. say("=" * 74)
  86. say(" 3. GET /payment/collection/order/%s/notify-logs (trace 集合改过)" % ORDER)
  87. say("=" * 74)
  88. st, logs = req("GET", "/payment/collection/order/%s/notify-logs" % ORDER, token)
  89. say(" HTTP %s" % st)
  90. say(json.dumps(logs, ensure_ascii=False, indent=2))
  91. lg = ((logs.get("data") if isinstance(logs, dict) else None) or [])
  92. say(" -> %d 行(改前只按订单 trace 查,是 4 行)" % len(lg))
  93. n_refund = 0
  94. for x in lg:
  95. tt = x.get("trade_type")
  96. if tt == "0":
  97. n_refund += 1
  98. say(" %s tt=%s verify=%s process=%s trace=%s"
  99. % (x.get("created_time"), tt, x.get("verify_status"),
  100. x.get("process_status"), x.get("pay_trace_no")))
  101. say(" 其中 trade_type=0(退款回调)的行 = %d" % n_refund)
  102. say("")
  103. say(" 判定:")
  104. say(" 退款单 %d 行(期望 1)、回调轨迹 %d 行含退款 %d 行(期望 6 / 2)"
  105. % (len(rows), len(lg), n_refund))
  106. say(" => %s" % ("两项都变了,用户报的两个症状在 HTTP 层确认修好"
  107. if len(rows) == 1 and n_refund == 2
  108. else "与期望不符,见上面原始读数"))
  109. io.open(OUT, "w", encoding="utf-8").write("\n".join(buf) + "\n")
  110. say("")
  111. say("正文已写入 %s" % os.path.relpath(OUT, ROOT))