| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181 |
- import datetime
- from fastapi import APIRouter, Depends, Query
- from typing import Any
- import json
- import os
- import logging
- from datetime import datetime
- from alien_store.api.deps import get_contract_service
- from alien_store.schemas.request.contract_store import TemplatesCreate, SignUrl
- from alien_store.services.contract_server import ContractServer
- from common.esigntool.main import *
- import re, urllib.parse
- # ------------------- 日志配置 -------------------
- LOG_DIR = os.path.join("common", "logs", "alien_store")
- os.makedirs(LOG_DIR, exist_ok=True)
- def _init_logger():
- logger = logging.getLogger("alien_store")
- if logger.handlers:
- return logger
- logger.setLevel(logging.INFO)
- fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s %(message)s")
- info_handler = logging.FileHandler(os.path.join(LOG_DIR, "info.log"), encoding="utf-8")
- info_handler.setLevel(logging.INFO)
- info_handler.setFormatter(fmt)
- error_handler = logging.FileHandler(os.path.join(LOG_DIR, "error.log"), encoding="utf-8")
- error_handler.setLevel(logging.ERROR)
- error_handler.setFormatter(fmt)
- logger.addHandler(info_handler)
- logger.addHandler(error_handler)
- # 控制台可选: logger.addHandler(logging.StreamHandler())
- return logger
- logger = _init_logger()
- router = APIRouter()
- @router.get("/")
- async def index():
- return {"module": "Contract", "status": "Ok"}
- @router.post("/get_esign_templates")
- async def create_esign_templates(templates_data: TemplatesCreate, templates_server: ContractServer = Depends(get_contract_service)):
- # AI审核完调用 e签宝生成文件
- logger.info(f"get_esign_templates request: {templates_data}")
- res_text = fill_in_template(templates_data.merchant_name)
- try:
- res_data = json.loads(res_text)
- except json.JSONDecodeError:
- logger.error(f"fill_in_template non-json resp: {res_text}")
- return {"success": False, "message": "e签宝返回非 JSON", "raw": res_text}
- # 从返回结构提取下载链接,需与实际返回字段匹配
- try:
- contract_url = res_data["data"]["fileDownloadUrl"]
- file_id = res_data["data"]["fileId"]
- m = re.search(r'/([^/]+)\.pdf', contract_url)
- if m:
- encoded_name = m.group(1)
- file_name = urllib.parse.unquote(encoded_name)
- except Exception:
- logger.error(f"fill_in_template missing fileDownloadUrl: {res_data}")
- return {"success": False, "message": "e签宝返回缺少 fileDownloadUrl", "raw": res_data}
- sign_data = create_by_file(file_id, file_name, templates_data.contact_phone, templates_data.merchant_name)
- try:
- sign_json = json.loads(sign_data)
- except json.JSONDecodeError:
- logger.error(f"create_by_file non-json resp: {sign_data}")
- return {"success": False, "message": "e签宝 create_by_file 返回非 JSON", "raw": sign_data}
- if not sign_json.get("data"):
- logger.error(f"create_by_file failed or missing data: {sign_json}")
- return {"success": False, "message": "e签宝创建签署流程失败", "raw": sign_json}
- sing_id = sign_json["data"].get("signFlowId")
- if not sing_id:
- logger.error(f"create_by_file missing signFlowId: {sign_json}")
- return {"success": False, "message": "e签宝返回缺少 signFlowId", "raw": sign_json}
- result_contract = {
- "contract_url": contract_url, # 合同模版链接
- "file_name": file_name, # 签署的合同的文件名称
- "file_id": file_id, # 生成的文件ID
- "status": 0, # 签署状态 0 未签署 1 已签署
- "sign_flow_id": sing_id, # 从
- "sign_url": "", # e签宝生成的签署页面
- "signing_time": "", # 签署合同的时间
- "effective_time": "", # 合同生效的时间
- "expiry_time": "", # 合同失效的时间
- "contract_download_url": "", # 合同签署完成后 下载文件的链接
- "is_master": 1 # 是否是入驻店铺的协议合同 是 1 否 0
- }
- updated = await templates_server.append_contract_url(templates_data, result_contract)
- logger.info(f"get_esign_templates success contact_phone={templates_data.contact_phone}, sign_flow_id={sing_id}")
- return {"success": True, "message": "合同模板已追加/创建", "sign_flow_id": sing_id, "file_id": file_id, "contract_url": contract_url}
- @router.get("/contracts/{store_id}")
- async def list_contracts(store_id: int, templates_server: ContractServer = Depends(get_contract_service)) -> Any:
- """根据 store_id 查询所有合同"""
- rows = await templates_server.list_by_store(store_id)
- return rows
- @router.get("/get_all_templates")
- async def get_all_templates(
- page: int = Query(1, ge=1, description="页码,从1开始"),
- page_size: int = Query(10, ge=1, le=100, description="每页条数,默认10"),
- templates_server: ContractServer = Depends(get_contract_service)
- ) -> Any:
- """分页查询所有合同"""
- return await templates_server.list_all_paged(page, page_size)
- @router.post("/esign/signurl")
- async def get_esign_sign_url(body: SignUrl, templates_server: ContractServer = Depends(get_contract_service)):
- """
- 当商家点击签署按钮时
- 携带合同相关的签署id和联系方式向e签宝发起请求
- 获取到签署的页面链接
- 并将签署url存入该合同对应的sign_url中
- """
- sing_flow_id = body.sign_flow_id
- contact_phone = body.contact_phone
- logger.info(f"esign/signurl request contact_phone={contact_phone}, sign_flow_id={sing_flow_id}")
- result = sign_url(sing_flow_id, contact_phone)
- try:
- result_json = json.loads(result)
- except json.JSONDecodeError:
- logger.error(f"sign_url non-json resp: {result}")
- return {"success": False, "message": "e签宝返回非JSON", "raw": result}
- data = result_json.get("data") if isinstance(result_json, dict) else None
- if not data or not data.get("url"):
- logger.error(f"sign_url missing url: {result_json}")
- return {"success": False, "message": "e签宝返回缺少签署链接", "raw": result_json}
- result_sign_url = data.get("url")
- await templates_server.update_sign_url(contact_phone, sing_flow_id, result_sign_url)
- logger.info(f"sign_url success contact_phone={contact_phone}, sign_flow_id={sing_flow_id}")
- return {"success": True, "data": {"url": result_sign_url}}
- @router.post("/esign/callback")
- async def esign_callback(payload: dict, templates_server: ContractServer = Depends(get_contract_service)) -> Any:
- """
- e签宝签署结果回调
- 需求:签署完成 -> 更新 signing_status=已签署,contract_url 中 status=1
- """
- sign_result = payload.get("signResult")
- operator = payload.get("operator") or {}
- sign_flow_id = payload.get("signFlowId")
- psn_account = operator.get("psnAccount") or {}
- contact_phone = psn_account.get("accountMobile")
- # 取回调中的毫秒时间戳,优先 operateTime,其次 timestamp
- ts_ms = payload.get("operateTime") or payload.get("timestamp")
- signing_dt = None
- if ts_ms:
- try:
- signing_dt = datetime.fromtimestamp(ts_ms / 1000)
- except Exception:
- signing_dt = None
- if sign_result == 2 and contact_phone and sign_flow_id:
- # 获取合同下载链接
- contract_download_url = None
- try:
- download_resp = file_download_url(sign_flow_id)
- download_json = json.loads(download_resp)
- if download_json.get("data") and download_json["data"].get("url"):
- contract_download_url = download_json["data"]["url"]
- except Exception as e:
- logger.error(f"file_download_url failed for sign_flow_id={sign_flow_id}: {e}")
-
- updated = await templates_server.mark_signed_by_phone(contact_phone, sign_flow_id, signing_dt, contract_download_url)
- logger.info(f"esign_callback success phone={contact_phone}, sign_flow_id={sign_flow_id}, updated={updated}")
- return {"code":"200","msg":"success"}
- logger.error(f"esign_callback ignored payload: {payload}")
- return {"success": False, "message": "未处理: signResult!=2 或手机号/签署流程缺失"}
- @router.post("/esign/callback_auth")
- async def esign_callback_auth(payload: dict, templates_server: ContractServer = Depends(get_contract_service)):
- logger.info(f"esign_callback_auth payload: {payload}")
- return {"code":"200","msg":"success"}
|