test_api_callback.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. """向对接系统发送一条与 Proposa API 当前格式一致的测试回调。
  2. 直接运行只预览 JSON;确认内容后增加 ``--send`` 才会真正发送。
  3. 最常修改的参数集中在下方“可编辑配置”区域。
  4. """
  5. from __future__ import annotations
  6. import argparse
  7. import json
  8. import sys
  9. from dataclasses import dataclass
  10. from urllib.error import HTTPError, URLError
  11. from urllib.request import Request, urlopen
  12. # ============================ 可编辑配置 ============================
  13. CALLBACK_URL = "http://121.43.55.7:10026/shenqin/tender/callback"
  14. CALLBACK_STATUS = "completed" # 可改为 "failed"
  15. TXB_ID = "4c9e748b-331f-468b-a735-d6748bc50cfd"
  16. REQUEST_ID = "test_callback_request_001"
  17. RESULT_PATH = "output/api_jobs/test_callback_request_001/测试招标文件.docx"
  18. FINAL_REVIEW_URL = (
  19. "http://127.0.0.1:8000/api/v1/jobs/test_callback_request_001/file"
  20. )
  21. ERROR_MESSAGE = "测试失败:Step1-6 处理进程异常退出(退出码 7)"
  22. TIMEOUT_SECONDS = 30
  23. # 文件名、大小和 SHA-256 均为测试数据,可按需直接修改。
  24. FILES = {
  25. "TENDER_FILE": {
  26. "filename": "测试招标文件.pdf",
  27. "size": 123456,
  28. "sha256": "1" * 64,
  29. },
  30. "PROCUREMENT_FILE": {
  31. "filename": "测试采购需求.docx",
  32. "size": 234567,
  33. "sha256": "2" * 64,
  34. },
  35. "REFERENCE_BID": {
  36. "filename": "测试参考投书.docx",
  37. "size": 345678,
  38. "sha256": "3" * 64,
  39. },
  40. }
  41. # ===================================================================
  42. @dataclass(frozen=True)
  43. class CallbackResponse:
  44. status: int
  45. body: str
  46. def build_callback_payload(status: str = CALLBACK_STATUS) -> dict:
  47. """按 src/http_api.py 当前成功/失败回调字段生成测试载荷。"""
  48. if status not in {"completed", "failed"}:
  49. raise ValueError("status 只能是 completed 或 failed")
  50. payload = {
  51. "request_id": REQUEST_ID,
  52. "txbId": TXB_ID,
  53. "status": status,
  54. "resultPath": RESULT_PATH,
  55. "files": FILES,
  56. }
  57. if status == "completed":
  58. payload["final_review_url"] = FINAL_REVIEW_URL
  59. else:
  60. payload["resultPath"] = f"error: {ERROR_MESSAGE}"
  61. payload["error"] = ERROR_MESSAGE
  62. return payload
  63. def send_callback(url: str, payload: dict) -> CallbackResponse:
  64. """使用与正式 API 相同的 POST JSON 编码发送,并返回接收端响应。"""
  65. body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
  66. request = Request(
  67. url,
  68. data=body,
  69. headers={"Content-Type": "application/json; charset=utf-8"},
  70. method="POST",
  71. )
  72. try:
  73. with urlopen(request, timeout=TIMEOUT_SECONDS) as response:
  74. response_body = response.read().decode("utf-8", errors="replace")
  75. return CallbackResponse(status=response.status, body=response_body)
  76. except HTTPError as exc:
  77. response_body = exc.read().decode("utf-8", errors="replace")
  78. return CallbackResponse(status=exc.code, body=response_body)
  79. except URLError as exc:
  80. raise RuntimeError(f"无法连接回调地址: {exc.reason}") from exc
  81. def main() -> int:
  82. if hasattr(sys.stdout, "reconfigure"):
  83. sys.stdout.reconfigure(encoding="utf-8")
  84. parser = argparse.ArgumentParser(description=__doc__)
  85. parser.add_argument("--send", action="store_true", help="实际发送;不传时只预览")
  86. parser.add_argument("--url", default=CALLBACK_URL, help="临时覆盖回调 URL")
  87. parser.add_argument(
  88. "--status",
  89. choices=("completed", "failed"),
  90. default=CALLBACK_STATUS,
  91. help="生成成功或失败回调",
  92. )
  93. args = parser.parse_args()
  94. payload = build_callback_payload(args.status)
  95. print(f"回调 URL: {args.url}")
  96. print(json.dumps(payload, ensure_ascii=False, indent=2))
  97. if not args.send:
  98. print("\n当前仅预览,确认后增加 --send 才会发送。")
  99. return 0
  100. response = send_callback(args.url, payload)
  101. print(f"\nHTTP 状态: {response.status}")
  102. print(f"响应内容: {response.body or '<空>'}")
  103. business_ok = True
  104. try:
  105. response_json = json.loads(response.body)
  106. except (json.JSONDecodeError, TypeError):
  107. response_json = None
  108. if isinstance(response_json, dict) and "code" in response_json:
  109. business_code = response_json["code"]
  110. business_ok = str(business_code) in {"0", "200"}
  111. print(f"业务码: {business_code}({'成功' if business_ok else '失败'})")
  112. return 0 if 200 <= response.status < 300 and business_ok else 1
  113. if __name__ == "__main__":
  114. raise SystemExit(main())