router.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import datetime
  2. from fastapi import APIRouter, Depends, Query
  3. from typing import Any, List, Union, Optional
  4. import json
  5. import os
  6. import logging
  7. from datetime import datetime
  8. from alien_store.api.deps import get_contract_service
  9. from alien_store.schemas.request.contract_store import TemplatesCreate, SignUrl
  10. from alien_store.schemas.response.contract_store import (
  11. ModuleStatusResponse,
  12. TemplatesCreateResponse,
  13. ErrorResponse,
  14. ContractStoreResponse,
  15. PaginatedResponse,
  16. SignUrlResponse,
  17. SuccessResponse
  18. )
  19. from alien_store.services.contract_server import ContractServer
  20. from common.esigntool.main import *
  21. import re, urllib.parse
  22. # ------------------- 日志配置 -------------------
  23. LOG_DIR = os.path.join("common", "logs", "alien_store")
  24. os.makedirs(LOG_DIR, exist_ok=True)
  25. def _init_logger():
  26. logger = logging.getLogger("alien_store")
  27. if logger.handlers:
  28. return logger
  29. logger.setLevel(logging.INFO)
  30. fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s %(message)s")
  31. info_handler = logging.FileHandler(os.path.join(LOG_DIR, "info.log"), encoding="utf-8")
  32. info_handler.setLevel(logging.INFO)
  33. info_handler.setFormatter(fmt)
  34. error_handler = logging.FileHandler(os.path.join(LOG_DIR, "error.log"), encoding="utf-8")
  35. error_handler.setLevel(logging.ERROR)
  36. error_handler.setFormatter(fmt)
  37. logger.addHandler(info_handler)
  38. logger.addHandler(error_handler)
  39. # 控制台可选: logger.addHandler(logging.StreamHandler())
  40. return logger
  41. logger = _init_logger()
  42. router = APIRouter()
  43. @router.get("/", response_model=ModuleStatusResponse)
  44. async def index() -> ModuleStatusResponse:
  45. return ModuleStatusResponse(module="Contract", status="Ok")
  46. @router.post("/get_esign_templates", response_model=Union[TemplatesCreateResponse, ErrorResponse])
  47. async def create_esign_templates(
  48. templates_data: TemplatesCreate,
  49. templates_server: ContractServer = Depends(get_contract_service)
  50. ) -> Union[TemplatesCreateResponse, ErrorResponse]:
  51. """AI审核完调用 e签宝生成文件"""
  52. logger.info(f"get_esign_templates request: {templates_data}")
  53. # res_text = fill_in_template(templates_data.merchant_name)
  54. res_text = fill_in_template(templates_data.store_name)
  55. try:
  56. res_data = json.loads(res_text)
  57. except json.JSONDecodeError:
  58. logger.error(f"fill_in_template non-json resp: {res_text}")
  59. return ErrorResponse(success=False, message="e签宝返回非 JSON", raw=res_text)
  60. # 从返回结构提取下载链接,需与实际返回字段匹配
  61. try:
  62. contract_url = res_data["data"]["fileDownloadUrl"]
  63. file_id = res_data["data"]["fileId"]
  64. m = re.search(r'/([^/]+)\.pdf', contract_url)
  65. if m:
  66. encoded_name = m.group(1)
  67. file_name = urllib.parse.unquote(encoded_name)
  68. except Exception:
  69. logger.error(f"fill_in_template missing fileDownloadUrl: {res_data}")
  70. return ErrorResponse(success=False, message="e签宝返回缺少 fileDownloadUrl", raw=res_data)
  71. # sign_data = create_by_file(file_id, file_name, templates_data.contact_phone, templates_data.merchant_name)
  72. sign_data = create_by_file(file_id, file_name, templates_data.contact_phone, templates_data.store_name, templates_data.merchant_name, templates_data.ord_id)
  73. try:
  74. sign_json = json.loads(sign_data)
  75. except json.JSONDecodeError:
  76. logger.error(f"create_by_file non-json resp: {sign_data}")
  77. return ErrorResponse(success=False, message="e签宝 create_by_file 返回非 JSON", raw=sign_data)
  78. if not sign_json.get("data"):
  79. logger.error(f"create_by_file failed or missing data: {sign_json}")
  80. return ErrorResponse(success=False, message="e签宝创建签署流程失败", raw=sign_json)
  81. sing_id = sign_json["data"].get("signFlowId")
  82. if not sing_id:
  83. logger.error(f"create_by_file missing signFlowId: {sign_json}")
  84. return ErrorResponse(success=False, message="e签宝返回缺少 signFlowId", raw=sign_json)
  85. result_contract = {
  86. "contract_url": contract_url, # 合同模版链接
  87. "file_name": file_name, # 签署的合同的文件名称
  88. "file_id": file_id, # 生成的文件ID
  89. "status": 0, # 签署状态 0 未签署 1 已签署
  90. "sign_flow_id": sing_id, # 从
  91. "sign_url": "", # e签宝生成的签署页面
  92. "signing_time": "", # 签署合同的时间
  93. "effective_time": "", # 合同生效的时间
  94. "expiry_time": "", # 合同失效的时间
  95. "contract_download_url": "", # 合同签署完成后 下载文件的链接
  96. "is_master": 1 # 是否是入驻店铺的协议合同 是 1 否 0
  97. }
  98. updated = await templates_server.append_contract_url(templates_data, result_contract)
  99. logger.info(f"get_esign_templates success contact_phone={templates_data.contact_phone}, sign_flow_id={sing_id}")
  100. return TemplatesCreateResponse(
  101. success=True,
  102. message="合同模板已追加/创建",
  103. sign_flow_id=sing_id,
  104. file_id=file_id,
  105. contract_url=contract_url
  106. )
  107. @router.get("/contracts/{store_id}", response_model=Union[List[ContractStoreResponse], dict])
  108. async def list_contracts(
  109. store_id: int,
  110. status: Optional[int] = Query(None, description="筛选合同状态:0 未签署,1 已签署"),
  111. templates_server: ContractServer = Depends(get_contract_service)
  112. ) -> Any:
  113. """根据 store_id 查询所有合同,支持根据 status 筛选"""
  114. # 1. 检查 store_info 中的审核状态
  115. reason = await templates_server.get_store_reason(store_id)
  116. if reason != "审核通过":
  117. return {"code": 555, "msg": "先进行认证"}
  118. # 2. 返回合同列表
  119. rows = await templates_server.list_by_store(store_id)
  120. if status is None:
  121. return [ContractStoreResponse(**row) for row in rows]
  122. # 3. 根据 status 筛选 contract_url 中的项
  123. filtered_rows = []
  124. for row in rows:
  125. contract_url_raw = row.get("contract_url")
  126. if not contract_url_raw:
  127. continue
  128. try:
  129. items = json.loads(contract_url_raw)
  130. if not isinstance(items, list):
  131. continue
  132. # 过滤列表中的合同项
  133. filtered_items = [item for item in items if item.get("status") == status]
  134. if filtered_items:
  135. # 只有当存在符合条件的合同时,才返回该记录,并只包含符合条件的合同项
  136. row_copy = dict(row)
  137. row_copy["contract_url"] = json.dumps(filtered_items, ensure_ascii=False)
  138. filtered_rows.append(ContractStoreResponse(**row_copy))
  139. except Exception as e:
  140. logger.error(f"Error filtering contracts for store_id {store_id}: {e}")
  141. continue
  142. return filtered_rows
  143. @router.get("/get_all_templates", response_model=PaginatedResponse)
  144. async def get_all_templates(
  145. page: int = Query(1, ge=1, description="页码,从1开始"),
  146. page_size: int = Query(10, ge=1, le=100, description="每页条数,默认10"),
  147. templates_server: ContractServer = Depends(get_contract_service)
  148. ) -> PaginatedResponse:
  149. """分页查询所有合同"""
  150. rows, total = await templates_server.list_all_paged(page, page_size)
  151. total_pages = (total + page_size - 1) // page_size if total > 0 else 0
  152. items = [ContractStoreResponse(**row) for row in rows]
  153. return PaginatedResponse(
  154. items=items,
  155. total=total,
  156. page=page,
  157. page_size=page_size,
  158. total_pages=total_pages
  159. )
  160. @router.post("/esign/signurl", response_model=Union[SignUrlResponse, ErrorResponse])
  161. async def get_esign_sign_url(
  162. body: SignUrl,
  163. templates_server: ContractServer = Depends(get_contract_service)
  164. ) -> Union[SignUrlResponse, ErrorResponse]:
  165. """
  166. 当商家点击签署按钮时
  167. 携带合同相关的签署id和联系方式向e签宝发起请求
  168. 获取到签署的页面链接
  169. 并将签署url存入该合同对应的sign_url中
  170. """
  171. sing_flow_id = body.sign_flow_id
  172. contact_phone = body.contact_phone
  173. logger.info(f"esign/signurl request contact_phone={contact_phone}, sign_flow_id={sing_flow_id}")
  174. result = sign_url(sing_flow_id, contact_phone)
  175. try:
  176. result_json = json.loads(result)
  177. except json.JSONDecodeError:
  178. logger.error(f"sign_url non-json resp: {result}")
  179. return ErrorResponse(success=False, message="e签宝返回非JSON", raw=result)
  180. data = result_json.get("data") if isinstance(result_json, dict) else None
  181. if not data or not data.get("url"):
  182. logger.error(f"sign_url missing url: {result_json}")
  183. return ErrorResponse(success=False, message="e签宝返回缺少签署链接", raw=result_json)
  184. result_sign_url = data.get("url")
  185. await templates_server.update_sign_url(contact_phone, sing_flow_id, result_sign_url)
  186. logger.info(f"sign_url success contact_phone={contact_phone}, sign_flow_id={sing_flow_id}")
  187. return SignUrlResponse(success=True, data={"url": result_sign_url})
  188. @router.post("/esign/callback", response_model=Union[SuccessResponse, ErrorResponse])
  189. async def esign_callback(
  190. payload: dict,
  191. templates_server: ContractServer = Depends(get_contract_service)
  192. ) -> Union[SuccessResponse, ErrorResponse]:
  193. """
  194. e签宝签署结果回调
  195. 需求:签署完成 -> 更新 signing_status=已签署,contract_url 中 status=1
  196. """
  197. sign_result = payload.get("signResult")
  198. operator = payload.get("operator") or {}
  199. sign_flow_id = payload.get("signFlowId")
  200. psn_account = operator.get("psnAccount") or {}
  201. contact_phone = psn_account.get("accountMobile")
  202. # 取回调中的毫秒时间戳,优先 operateTime,其次 timestamp
  203. ts_ms = payload.get("operateTime") or payload.get("timestamp")
  204. signing_dt = None
  205. if ts_ms:
  206. try:
  207. signing_dt = datetime.fromtimestamp(ts_ms / 1000)
  208. except Exception:
  209. signing_dt = None
  210. if sign_result == 2:
  211. # 获取合同下载链接
  212. contract_download_url = None
  213. try:
  214. download_resp = file_download_url(sign_flow_id)
  215. download_json = json.loads(download_resp)
  216. contract_download_url = download_json["data"]["files"][0]["downloadUrl"]
  217. except Exception as e:
  218. logger.error(f"file_download_url failed for sign_flow_id={sign_flow_id}: {e}")
  219. print(contract_download_url,666666666666666666666666666666)
  220. updated = await templates_server.mark_signed_by_phone(contact_phone, sign_flow_id, signing_dt, contract_download_url)
  221. logger.info(f"esign_callback success phone={contact_phone}, sign_flow_id={sign_flow_id}, updated={updated}")
  222. return SuccessResponse(code="200", msg="success")
  223. logger.error(f"esign_callback ignored payload: {payload}")
  224. return ErrorResponse(success=False, message="未处理: signResult!=2 或手机号/签署流程缺失")
  225. @router.post("/esign/callback_auth", response_model=SuccessResponse)
  226. async def esign_callback_auth(
  227. payload: dict,
  228. templates_server: ContractServer = Depends(get_contract_service)
  229. ) -> SuccessResponse:
  230. logger.info(f"esign_callback_auth payload: {payload}")
  231. return SuccessResponse(code="200", msg="success")