瀏覽代碼

Merge remote-tracking branch 'origin/sit' into uat

# Conflicts:
#	alien_store/api/router.py
mengqiankang 1 月之前
父節點
當前提交
5b9ebdb3b8
共有 5 個文件被更改,包括 255 次插入108 次删除
  1. 141 100
      alien_store/api/router.py
  2. 13 6
      alien_store/repositories/contract_repo.py
  3. 17 2
      alien_store/schemas/request/contract_store.py
  4. 10 0
      common/esigntool/main.py
  5. 74 0
      test.py

+ 141 - 100
alien_store/api/router.py

@@ -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,

+ 13 - 6
alien_store/repositories/contract_repo.py

@@ -281,12 +281,16 @@ class ContractRepository:
 
     async def append_contract_url(self, templates_data, contract_item: dict):
         """
-        根据手机号,向 contract_url(JSON 列表)追加新的合同信息;
-        若手机号不存在,则创建新记录。
+        根据 store_id,向 contract_url(JSON 列表)追加新的合同信息;
+        若 store_id 不存在,则创建新记录。
         """
-        contact_phone = getattr(templates_data, "contact_phone", None)
+        store_id = getattr(templates_data, "store_id", None)
+        if store_id is None:
+            logger.error("append_contract_url missing store_id")
+            return False
+
         result = await self._execute_with_retry(
-            ContractStore.__table__.select().where(ContractStore.contact_phone == contact_phone)
+            ContractStore.__table__.select().where(ContractStore.store_id == store_id)
         )
         rows = result.mappings().all()
         updated = False
@@ -304,6 +308,9 @@ class ContractRepository:
                 update_values = {"contract_url": json.dumps(items, ensure_ascii=False)}
                 if store_name:
                     update_values["store_name"] = store_name
+                contact_phone = getattr(templates_data, "contact_phone", None)
+                if contact_phone:
+                    update_values["contact_phone"] = contact_phone
                 await self._execute_with_retry(
                     ContractStore.__table__.update()
                     .where(ContractStore.id == row["id"])
@@ -315,11 +322,11 @@ class ContractRepository:
             return updated
         # 未找到则创建新记录
         new_record = ContractStore(
-            store_id=getattr(templates_data, "store_id", None),
+            store_id=store_id,
             store_name=store_name,
             business_segment=getattr(templates_data, "business_segment", None),
             merchant_name=getattr(templates_data, "merchant_name", None),
-            contact_phone=contact_phone,
+            contact_phone=getattr(templates_data, "contact_phone", None),
             contract_url=json.dumps([contract_item], ensure_ascii=False),
             seal_url='0.0',
             signing_status='未签署'

+ 17 - 2
alien_store/schemas/request/contract_store.py

@@ -1,10 +1,11 @@
-from pydantic import BaseModel, EmailStr, Field, field_validator
+import re
+from pydantic import BaseModel, Field, field_validator
 
 
 
 class TemplatesCreate(BaseModel):
     """模板创建请求模型"""
-    store_id: int = Field(description="入驻店铺ID")
+    store_id: int = Field(gt=0, description="入驻店铺ID")
     store_name: str = Field(description="商家店铺名称")
     business_segment: str = Field(description="入驻店铺经营板块")
     merchant_name: str = Field(description="商家姓名")
@@ -13,6 +14,20 @@ class TemplatesCreate(BaseModel):
     seal_url: str | None = Field(default=None, description="印章文件地址")
     ord_id: str = Field(description="企业用户的统一社会信用代码")
 
+    @field_validator("contact_phone")
+    @classmethod
+    def validate_contact_phone(cls, value: str) -> str:
+        if not re.fullmatch(r"^1\d{10}$", value):
+            raise ValueError("contact_phone 格式错误,应为11位手机号")
+        return value
+
+    @field_validator("ord_id")
+    @classmethod
+    def validate_ord_id(cls, value: str) -> str:
+        if not re.fullmatch(r"^[0-9A-Z]{18}$", value):
+            raise ValueError("ord_id 格式错误,应为18位大写字母或数字")
+        return value
+
 class SignUrl(BaseModel):
     """签署合同页面请求模型"""
     sign_flow_id: str = Field(description="合同相关的签署id")

+ 10 - 0
common/esigntool/main.py

@@ -97,6 +97,16 @@ def fill_in_template(name):
     print(resp.text)
     return resp.text
 
+def get_contract_detail(file_id):
+    """查询PDF模板填写后文件"""
+    api_path = f"/v3/files/{file_id}"
+    method = "GET"
+    json_headers = buildSignJsonHeader(config.appId, config.scert, method, api_path)
+    resp = requests.request(method, config.host + api_path, headers=json_headers)
+    print(resp.text)
+    return resp.text
+
+# get_contract_detail("f0371b4ae7c64c8ca16be3bf031d1d6e")
 # def create_by_file(file_id, file_name,  contact_phone, merchant_name):
 #     """基于文件发起签署"""
 #     api_path = "/v3/sign-flow/create-by-file"

+ 74 - 0
test.py

@@ -0,0 +1,74 @@
+import requests
+
+
+# url = "http://127.0.0.1:8001/api/store/get_esign_templates"
+# url = "http://120.26.186.130:33333/api/store/get_esign_templates"
+# """
+# 商家入驻AI审核通过后, AI携带真实有效的信息调用此接口,数据库 store_contract 生成对应的数据
+# """
+# body = {
+#     "store_id": 666,
+#     "store_name": "爱丽恩严(大连)商务科技有限公司深圳分公司",
+#     "business_segment": "生活服务",
+#     "merchant_name": "彭少荣",
+#     "contact_phone": "13923864580",
+#     "ord_id": "91440300MADDW7XC4C"
+# }
+# res = requests.post(url, json=body)
+# print(res.text)
+
+# ----------------------------------------------------------------------------------------------------------------------
+# url = "http://127.0.0.1:8001/api/store/contracts/381"
+# url = "http://120.26.186.130:33333/api/store/contracts/381?status=0"
+# """
+# 商家点击合同管理模块时,可以条件筛选查询 未签署合同和已签署合同
+# status:签署状态 0 未签署 1 已签署
+# page:分页查询 页码 默认1
+# page_size:分页查询 每页数量 默认10
+# """
+# resp = requests.get(url)
+# print(resp.text)
+
+# ----------------------------------------------------------------------------------------------------------------------
+url = "http://127.0.0.1:8001/api/store/contracts/detail/d43a18e1e9164a7d8dab4ca5c49d9960"
+# url = "http://120.26.186.130:33333/api/store/contracts/detail/aea4124ac4614936b8625fedb64bac47"
+"""
+商家点击到某个具体的合同 向这个接口发送请求
+由于e签宝提供的合同模板URL会过期,所以会再次携带flow_id 向e签宝发起请求刷新新的URL
+下载URL同理
+"""
+resp = requests.get(url)
+print(resp.text)
+
+# ----------------------------------------------------------------------------------------------------------------------
+# url = "http://127.0.0.1:8001/api/store/esign/signurl"
+# url = "http://120.26.186.130:33333/api/store/esign/signurl"
+# body = {
+#     "sign_flow_id": "8980608cb10c448cbdf96aa7df51179b",
+#     "contact_phone": "13923864580"
+# }
+# """
+# 当商家点击签署按钮时
+# 携带合同相关的签署id和联系方式向e签宝发起请求
+# 获取到签署的页面链接
+# 并将签署url存入该合同对应的sign_url中
+# """
+# resp = requests.post(url, json=body)
+# print(resp.text)
+
+#-----------------------------------------------------------------------------------------------------------------------
+# url = "http://127.0.0.1:8001/api/store/get_all_templates"
+# url = "http://120.26.186.130:33333/api/store/get_all_templates"
+# params = {
+#     "page": 1, # 页码 默认1
+#     "page_size": 10, # 每页条数 默认10
+#     "store": "", # 店铺名称(模糊查询)
+#     "merchant_name": "", # 商家名称(模糊查询)
+#     "signing_status": "", # 签署状态
+#     "business_segment": "", # 经营板块
+#     "store_status": "", # 店铺状态:正常/禁用
+#     "expiry_start": "", # 到期时间起
+#     "expiry_end": "" # 到期时间止
+# }
+# resp = requests.get(url)
+# print(resp.text)