|
|
@@ -3,19 +3,20 @@ from fastapi import APIRouter, Depends, Query
|
|
|
from typing import Any, Union, Optional
|
|
|
import os
|
|
|
import logging
|
|
|
+from pydantic import ValidationError
|
|
|
from alien_store.api.deps import get_contract_service
|
|
|
-from alien_store.schemas.request.contract_store import TemplatesCreate, SignUrl
|
|
|
+from alien_store.schemas.request.contract_store import TemplatesCreate
|
|
|
from alien_store.schemas.response.contract_store import (
|
|
|
ModuleStatusResponse,
|
|
|
TemplatesCreateResponse,
|
|
|
ErrorResponse,
|
|
|
ContractStoreResponse,
|
|
|
PaginatedResponse,
|
|
|
- SignUrlResponse,
|
|
|
SuccessResponse
|
|
|
)
|
|
|
from alien_store.services.contract_server import ContractServer
|
|
|
from common.esigntool.main import *
|
|
|
+from common.esigntool import main as esign_main
|
|
|
import re, urllib.parse
|
|
|
|
|
|
# ------------------- 日志配置 -------------------
|
|
|
@@ -43,16 +44,42 @@ logger = _init_logger()
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
+
|
|
|
+def _format_validation_errors(exc: ValidationError) -> list[dict[str, str]]:
|
|
|
+ errors = []
|
|
|
+ for err in exc.errors():
|
|
|
+ loc = err.get("loc", ())
|
|
|
+ field = ".".join(str(item) for item in loc if item != "body")
|
|
|
+ errors.append(
|
|
|
+ {
|
|
|
+ "field": field or "body",
|
|
|
+ "type": err.get("type", "validation_error"),
|
|
|
+ "message": err.get("msg", "参数校验失败"),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ return errors
|
|
|
+
|
|
|
@router.get("/", response_model=ModuleStatusResponse)
|
|
|
async def index() -> ModuleStatusResponse:
|
|
|
return ModuleStatusResponse(module="Contract", status="Ok")
|
|
|
|
|
|
@router.post("/get_esign_templates", response_model=Union[TemplatesCreateResponse, ErrorResponse])
|
|
|
async def create_esign_templates(
|
|
|
- templates_data: TemplatesCreate,
|
|
|
+ templates_data: dict[str, Any],
|
|
|
templates_server: ContractServer = Depends(get_contract_service)
|
|
|
) -> Union[TemplatesCreateResponse, ErrorResponse]:
|
|
|
"""AI审核完调用 e签宝生成文件"""
|
|
|
+ try:
|
|
|
+ templates_data = TemplatesCreate.model_validate(templates_data)
|
|
|
+ except ValidationError as e:
|
|
|
+ detail = _format_validation_errors(e)
|
|
|
+ logger.error("get_esign_templates validation failed: %s", detail)
|
|
|
+ return ErrorResponse(
|
|
|
+ success=False,
|
|
|
+ message="请求参数校验失败",
|
|
|
+ raw={"errors": detail},
|
|
|
+ )
|
|
|
+
|
|
|
logger.info(f"get_esign_templates request: {templates_data}")
|
|
|
# res_text = fill_in_template(templates_data.merchant_name)
|
|
|
res_text = fill_in_template(templates_data.store_name)
|
|
|
@@ -75,6 +102,7 @@ async def create_esign_templates(
|
|
|
|
|
|
# sign_data = create_by_file(file_id, file_name, templates_data.contact_phone, templates_data.merchant_name)
|
|
|
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)
|
|
|
+ print(sign_data)
|
|
|
try:
|
|
|
sign_json = json.loads(sign_data)
|
|
|
except json.JSONDecodeError:
|
|
|
@@ -122,57 +150,68 @@ async def list_contracts(
|
|
|
templates_server: ContractServer = Depends(get_contract_service)
|
|
|
) -> Any:
|
|
|
"""根据 store_id 查询所有合同,支持根据 status 筛选和分页"""
|
|
|
- # 1. 检查 store_info 中的审核状态
|
|
|
- reason = await templates_server.get_store_reason(store_id)
|
|
|
- if reason != "审核通过":
|
|
|
- return {"code": 555, "msg": "先进行认证"}
|
|
|
-
|
|
|
- # 2. 返回合同列表
|
|
|
- rows = await templates_server.list_by_store(store_id)
|
|
|
-
|
|
|
- all_filtered_items = []
|
|
|
- # 3. 解析并筛选所有符合条件的合同项
|
|
|
- for row in rows:
|
|
|
- contract_url_raw = row.get("contract_url")
|
|
|
- if not contract_url_raw:
|
|
|
- continue
|
|
|
- try:
|
|
|
- items = json.loads(contract_url_raw)
|
|
|
- if not isinstance(items, list):
|
|
|
+ logger.info(
|
|
|
+ "list_contracts request store_id=%s status=%s page=%s page_size=%s",
|
|
|
+ store_id,
|
|
|
+ status,
|
|
|
+ page,
|
|
|
+ page_size,
|
|
|
+ )
|
|
|
+ try:
|
|
|
+ # 1. 检查 store_info 中的审核状态
|
|
|
+ reason = await templates_server.get_store_reason(store_id)
|
|
|
+ if reason != "审核通过":
|
|
|
+ return {"code": 555, "msg": "先进行认证", "reason": reason}
|
|
|
+
|
|
|
+ # 2. 返回合同列表
|
|
|
+ rows = await templates_server.list_by_store(store_id)
|
|
|
+
|
|
|
+ all_filtered_items = []
|
|
|
+ # 3. 解析并筛选所有符合条件的合同项
|
|
|
+ for row in rows:
|
|
|
+ contract_url_raw = row.get("contract_url")
|
|
|
+ if not contract_url_raw:
|
|
|
continue
|
|
|
-
|
|
|
- for item in items:
|
|
|
- # 如果传了 status,则进行筛选
|
|
|
- if status is not None and item.get("status") != status:
|
|
|
+ try:
|
|
|
+ items = json.loads(contract_url_raw)
|
|
|
+ if not isinstance(items, list):
|
|
|
continue
|
|
|
|
|
|
- # 将店铺基础信息混入每个合同项中,方便前端展示
|
|
|
- item_with_info = dict(item)
|
|
|
- item_with_info["id"] = row.get("id")
|
|
|
- item_with_info["store_id"] = row.get("store_id")
|
|
|
- item_with_info["store_name"] = row.get("store_name")
|
|
|
- item_with_info["merchant_name"] = row.get("merchant_name")
|
|
|
- item_with_info["contact_phone"] = row.get("contact_phone")
|
|
|
- all_filtered_items.append(item_with_info)
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Error processing contracts for store_id {store_id}: {e}")
|
|
|
- continue
|
|
|
-
|
|
|
- # 4. 手动分页
|
|
|
- total = len(all_filtered_items)
|
|
|
- start = (page - 1) * page_size
|
|
|
- end = start + page_size
|
|
|
- paged_items = all_filtered_items[start:end]
|
|
|
-
|
|
|
- total_pages = (total + page_size - 1) // page_size if total > 0 else 0
|
|
|
+ for item in items:
|
|
|
+ # 如果传了 status,则进行筛选
|
|
|
+ if status is not None and item.get("status") != status:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 将店铺基础信息混入每个合同项中,方便前端展示
|
|
|
+ item_with_info = dict(item)
|
|
|
+ item_with_info["id"] = row.get("id")
|
|
|
+ item_with_info["store_id"] = row.get("store_id")
|
|
|
+ item_with_info["store_name"] = row.get("store_name")
|
|
|
+ item_with_info["merchant_name"] = row.get("merchant_name")
|
|
|
+ item_with_info["contact_phone"] = row.get("contact_phone")
|
|
|
+ all_filtered_items.append(item_with_info)
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error processing contracts for store_id {store_id}: {e}", exc_info=True)
|
|
|
+ continue
|
|
|
|
|
|
- return {
|
|
|
- "items": paged_items,
|
|
|
- "total": total,
|
|
|
- "page": page,
|
|
|
- "page_size": page_size,
|
|
|
- "total_pages": total_pages
|
|
|
- }
|
|
|
+ # 4. 手动分页
|
|
|
+ total = len(all_filtered_items)
|
|
|
+ start = (page - 1) * page_size
|
|
|
+ end = start + page_size
|
|
|
+ paged_items = all_filtered_items[start:end]
|
|
|
+
|
|
|
+ total_pages = (total + page_size - 1) // page_size if total > 0 else 0
|
|
|
+
|
|
|
+ return {
|
|
|
+ "items": paged_items,
|
|
|
+ "total": total,
|
|
|
+ "page": page,
|
|
|
+ "page_size": page_size,
|
|
|
+ "total_pages": total_pages
|
|
|
+ }
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"list_contracts failed store_id={store_id}: {e}", exc_info=True)
|
|
|
+ return {"code": 500, "msg": "查询合同失败", "error": str(e)}
|
|
|
|
|
|
@router.get("/contracts/detail/{sign_flow_id}", response_model=Union[dict, ErrorResponse])
|
|
|
async def get_contract_detail(
|
|
|
@@ -190,10 +229,58 @@ async def get_contract_detail(
|
|
|
|
|
|
status = item.get("status")
|
|
|
if status == 0:
|
|
|
+ file_id = item.get("file_id")
|
|
|
+ if not file_id:
|
|
|
+ return ErrorResponse(success=False, message="缺少 file_id,无法获取合同详情")
|
|
|
+ try:
|
|
|
+ detail_resp = esign_main.get_contract_detail(file_id)
|
|
|
+ detail_json = json.loads(detail_resp)
|
|
|
+ data = detail_json.get("data") if isinstance(detail_json, dict) else None
|
|
|
+ contract_url = None
|
|
|
+ if isinstance(data, dict):
|
|
|
+ contract_url = data.get("fileDownloadUrl")
|
|
|
+ if not contract_url and isinstance(detail_json, dict):
|
|
|
+ contract_url = detail_json.get("fileDownloadUrl")
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"get_contract_detail failed file_id={file_id}: {e}")
|
|
|
+ return ErrorResponse(success=False, message="获取合同链接失败", raw=str(e))
|
|
|
+
|
|
|
+ if not contract_url:
|
|
|
+ logger.error(f"get_contract_detail missing contract_url file_id={file_id}: {detail_resp}")
|
|
|
+ return ErrorResponse(success=False, message="e签宝返回缺少合同链接", raw=detail_resp)
|
|
|
+
|
|
|
+ if row and isinstance(items, list):
|
|
|
+ for it in items:
|
|
|
+ if it.get("sign_flow_id") == sign_flow_id:
|
|
|
+ it["contract_url"] = contract_url
|
|
|
+ break
|
|
|
+ await templates_server.update_contract_items(row["id"], items)
|
|
|
+
|
|
|
+ # 融合原 /esign/signurl 逻辑:调用 e签宝 获取签署链接并落库
|
|
|
+ contact_phone = item.get("contact_phone") or (row.get("contact_phone") if isinstance(row, dict) else None)
|
|
|
+ if not contact_phone:
|
|
|
+ return ErrorResponse(success=False, message="缺少 contact_phone,无法获取签署链接")
|
|
|
+ try:
|
|
|
+ sign_resp = sign_url(sign_flow_id, contact_phone)
|
|
|
+ sign_json = json.loads(sign_resp)
|
|
|
+ except json.JSONDecodeError:
|
|
|
+ logger.error(f"sign_url non-json resp: {sign_resp}")
|
|
|
+ return ErrorResponse(success=False, message="e签宝返回非JSON", raw=sign_resp)
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"sign_url failed sign_flow_id={sign_flow_id}, contact_phone={contact_phone}: {e}")
|
|
|
+ return ErrorResponse(success=False, message="获取签署链接失败", raw=str(e))
|
|
|
+
|
|
|
+ sign_data = sign_json.get("data") if isinstance(sign_json, dict) else None
|
|
|
+ result_sign_url = sign_data.get("url") if isinstance(sign_data, dict) else None
|
|
|
+ if not result_sign_url:
|
|
|
+ logger.error(f"sign_url missing url: {sign_json}")
|
|
|
+ return ErrorResponse(success=False, message="e签宝返回缺少签署链接", raw=sign_json)
|
|
|
+ await templates_server.update_sign_url(contact_phone, sign_flow_id, result_sign_url)
|
|
|
+
|
|
|
return {
|
|
|
"status": 0,
|
|
|
- "contract_url": item.get("contract_url", ""),
|
|
|
- "sign_url": item.get("sign_url", ""),
|
|
|
+ "contract_url": contract_url,
|
|
|
+ "sign_url": result_sign_url,
|
|
|
"sign_flow_id": sign_flow_id
|
|
|
}
|
|
|
|
|
|
@@ -205,16 +292,17 @@ async def get_contract_detail(
|
|
|
except Exception as e:
|
|
|
logger.error(f"file_download_url failed sign_flow_id={sign_flow_id}: {e}")
|
|
|
return ErrorResponse(success=False, message="获取合同下载链接失败", raw=str(e))
|
|
|
-
|
|
|
if row and isinstance(items, list):
|
|
|
for it in items:
|
|
|
if it.get("sign_flow_id") == sign_flow_id:
|
|
|
it["contract_download_url"] = contract_download_url
|
|
|
+ it["contract_url"] = contract_download_url # 与 status=0 一致,用 file_download_url 更新最新 contract_url
|
|
|
break
|
|
|
await templates_server.update_contract_items(row["id"], items)
|
|
|
|
|
|
return {
|
|
|
"status": 1,
|
|
|
+ "contract_url": contract_download_url,
|
|
|
"contract_download_url": contract_download_url,
|
|
|
"sign_flow_id": sign_flow_id
|
|
|
}
|
|
|
@@ -257,36 +345,6 @@ async def get_all_templates(
|
|
|
total_pages=total_pages
|
|
|
)
|
|
|
|
|
|
-@router.post("/esign/signurl", response_model=Union[SignUrlResponse, ErrorResponse])
|
|
|
-async def get_esign_sign_url(
|
|
|
- body: SignUrl,
|
|
|
- templates_server: ContractServer = Depends(get_contract_service)
|
|
|
-) -> Union[SignUrlResponse, ErrorResponse]:
|
|
|
- """
|
|
|
- 当商家点击签署按钮时
|
|
|
- 携带合同相关的签署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 ErrorResponse(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 ErrorResponse(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 SignUrlResponse(success=True, data={"url": result_sign_url})
|
|
|
-
|
|
|
@router.post("/esign/callback", response_model=Union[SuccessResponse, ErrorResponse])
|
|
|
async def esign_callback(
|
|
|
payload: dict,
|
|
|
@@ -327,23 +385,6 @@ async def esign_callback(
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
# @router.post("/esign/callback_auth", response_model=SuccessResponse)
|
|
|
# async def esign_callback_auth(
|
|
|
# payload: dict,
|