contract_builder.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import json
  2. import logging
  3. import re
  4. import urllib.parse
  5. from typing import Any
  6. from alien_contract.infrastructure.esign.main import SIGN_POSITIONS, apply_platform_seal, fill_in_template, create_by_file
  7. from alien_contract.infrastructure.esign.esign_config import Config
  8. logger = logging.getLogger(__name__)
  9. cfg = Config()
  10. class ContractBuildError(Exception):
  11. def __init__(self, message: str, raw: Any):
  12. super().__init__(message)
  13. self.message = message
  14. self.raw = raw
  15. def _is_esign_success(resp: dict[str, Any]) -> bool:
  16. code = resp.get("code")
  17. return code in (0, "0") or resp.get("success") is True
  18. def _apply_platform_seal_if_required(contract_type: str, contract_name: str, sign_flow_id: str) -> None:
  19. positions = SIGN_POSITIONS.get(contract_type, {})
  20. if not positions.get("alien"):
  21. return
  22. seal_resp = apply_platform_seal(sign_flow_id)
  23. try:
  24. seal_json = json.loads(seal_resp)
  25. except json.JSONDecodeError:
  26. logger.error("apply_platform_seal non-json resp contract_type=%s: %s", contract_type, seal_resp)
  27. raise ContractBuildError(
  28. message=f"{contract_name}平台自动盖章失败:e签宝返回非 JSON",
  29. raw={"contract_type": contract_type, "sign_flow_id": sign_flow_id, "resp": seal_resp},
  30. )
  31. if not isinstance(seal_json, dict) or not _is_esign_success(seal_json):
  32. logger.error("apply_platform_seal failed contract_type=%s: %s", contract_type, seal_json)
  33. raise ContractBuildError(
  34. message=f"{contract_name}平台自动盖章失败",
  35. raw={"contract_type": contract_type, "sign_flow_id": sign_flow_id, "resp": seal_json},
  36. )
  37. def build_contract_items(
  38. configs: list[tuple[str, str, int]],
  39. template_name: str,
  40. signer_name: str,
  41. signer_id_num: str,
  42. psn_account: str,
  43. psn_name: str,
  44. ) -> list[dict[str, Any]]:
  45. items: list[dict[str, Any]] = []
  46. for contract_type, contract_name, is_master in configs:
  47. res_text = fill_in_template(template_name, contract_type=contract_type)
  48. try:
  49. res_data = json.loads(res_text)
  50. except json.JSONDecodeError:
  51. logger.error("fill_in_template non-json resp contract_type=%s: %s", contract_type, res_text)
  52. raise ContractBuildError(
  53. message=f"{contract_name}生成失败:e签宝返回非 JSON",
  54. raw={"contract_type": contract_type, "resp": res_text},
  55. )
  56. try:
  57. contract_url = res_data["data"]["fileDownloadUrl"]
  58. file_id = res_data["data"]["fileId"]
  59. m = re.search(r"/([^/]+)\.pdf", contract_url)
  60. if m:
  61. encoded_name = m.group(1)
  62. file_name = urllib.parse.unquote(encoded_name)
  63. else:
  64. file_name = f"{contract_type}.pdf"
  65. except Exception:
  66. logger.error("fill_in_template missing fileDownloadUrl contract_type=%s: %s", contract_type, res_data)
  67. template_id = cfg.templates_map.get(contract_type)
  68. code = res_data.get("code") if isinstance(res_data, dict) else None
  69. if code == 404:
  70. message = f"{contract_name}生成失败:模板不存在或无权限(template_id={template_id})"
  71. elif code == 401:
  72. message = f"{contract_name}生成失败:签名校验失败(template_id={template_id})"
  73. else:
  74. message = f"{contract_name}生成失败:e签宝返回缺少 fileDownloadUrl"
  75. raise ContractBuildError(
  76. message=message,
  77. raw={"contract_type": contract_type, "template_id": template_id, "resp": res_data},
  78. )
  79. sign_data = create_by_file(
  80. file_id,
  81. file_name,
  82. signer_name,
  83. signer_id_num,
  84. psn_account,
  85. psn_name,
  86. contract_type=contract_type,
  87. )
  88. try:
  89. sign_json = json.loads(sign_data)
  90. except json.JSONDecodeError:
  91. logger.error("create_by_file non-json resp contract_type=%s: %s", contract_type, sign_data)
  92. raise ContractBuildError(
  93. message=f"{contract_name}发起签署失败:e签宝返回非 JSON",
  94. raw={"contract_type": contract_type, "resp": sign_data},
  95. )
  96. if not sign_json.get("data"):
  97. logger.error("create_by_file failed or missing data contract_type=%s: %s", contract_type, sign_json)
  98. resp_code = sign_json.get("code") if isinstance(sign_json, dict) else None
  99. resp_msg = sign_json.get("message") if isinstance(sign_json, dict) else None
  100. if resp_code == 1437328:
  101. message = f"{contract_name}发起签署失败:手机号与真实姓名不匹配,请确认联系人实名信息"
  102. elif resp_code == 1435002:
  103. message = f"{contract_name}发起签署失败:签署参数不合法({resp_msg})"
  104. elif resp_code == 1437306:
  105. message = f"{contract_name}发起签署失败:签章定位页超出文档页数,请检查模板页码配置"
  106. elif resp_code == 1435011:
  107. message = f"{contract_name}发起签署失败:签署文件或签署人为空,请检查签章坐标与签署方配置"
  108. elif resp_msg == "EMPTY_RESPONSE":
  109. message = f"{contract_name}发起签署失败:e签宝返回空响应,请检查签署参数与账号配置"
  110. else:
  111. message = f"{contract_name}发起签署失败"
  112. raise ContractBuildError(
  113. message=message,
  114. raw={"contract_type": contract_type, "resp": sign_json},
  115. )
  116. sign_id = sign_json["data"].get("signFlowId")
  117. if not sign_id:
  118. logger.error("create_by_file missing signFlowId contract_type=%s: %s", contract_type, sign_json)
  119. raise ContractBuildError(
  120. message=f"{contract_name}发起签署失败:e签宝返回缺少 signFlowId",
  121. raw={"contract_type": contract_type, "resp": sign_json},
  122. )
  123. _apply_platform_seal_if_required(contract_type, contract_name, sign_id)
  124. items.append(
  125. {
  126. "contract_type": contract_type,
  127. "contract_name": contract_name,
  128. "contract_url": contract_url,
  129. "file_name": file_name,
  130. "file_id": file_id,
  131. "status": 0,
  132. "sign_flow_id": sign_id,
  133. "sign_url": "",
  134. "signing_time": "",
  135. "effective_time": "",
  136. "expiry_time": "",
  137. "contract_download_url": "",
  138. "is_master": is_master,
  139. }
  140. )
  141. return items