Sfoglia il codice sorgente

加入响应校验

mengqiankang 2 mesi fa
parent
commit
9bc448d50f

+ 72 - 32
alien_store/api/router.py

@@ -1,12 +1,21 @@
 import datetime
 from fastapi import APIRouter, Depends, Query
-from typing import Any
+from typing import Any, List, Union
 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.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 *
 import re, urllib.parse
@@ -36,12 +45,15 @@ logger = _init_logger()
 
 router = APIRouter()
 
-@router.get("/")
-async def index():
-    return {"module": "Contract", "status": "Ok"}
+@router.get("/", response_model=ModuleStatusResponse)
+async def index() -> ModuleStatusResponse:
+    return ModuleStatusResponse(module="Contract", status="Ok")
 
-@router.post("/get_esign_templates")
-async def create_esign_templates(templates_data: TemplatesCreate, templates_server: ContractServer = Depends(get_contract_service)):
+@router.post("/get_esign_templates", response_model=Union[TemplatesCreateResponse, ErrorResponse])
+async def create_esign_templates(
+    templates_data: TemplatesCreate, 
+    templates_server: ContractServer = Depends(get_contract_service)
+) -> Union[TemplatesCreateResponse, ErrorResponse]:
     # AI审核完调用 e签宝生成文件
     logger.info(f"get_esign_templates request: {templates_data}")
     res_text = fill_in_template(templates_data.merchant_name)
@@ -49,7 +61,7 @@ async def create_esign_templates(templates_data: TemplatesCreate, templates_serv
         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}
+        return ErrorResponse(success=False, message="e签宝返回非 JSON", raw=res_text)
     # 从返回结构提取下载链接,需与实际返回字段匹配
     try:
         contract_url = res_data["data"]["fileDownloadUrl"]
@@ -60,23 +72,23 @@ async def create_esign_templates(templates_data: TemplatesCreate, templates_serv
             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}
+        return ErrorResponse(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}
+        return ErrorResponse(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}
+        return ErrorResponse(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}
+        return ErrorResponse(success=False, message="e签宝返回缺少 signFlowId", raw=sign_json)
 
     result_contract = {
         "contract_url": contract_url, # 合同模版链接
@@ -93,25 +105,47 @@ async def create_esign_templates(templates_data: TemplatesCreate, templates_serv
     }
     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:
+    return TemplatesCreateResponse(
+        success=True,
+        message="合同模板已追加/创建",
+        sign_flow_id=sing_id,
+        file_id=file_id,
+        contract_url=contract_url
+    )
+
+@router.get("/contracts/{store_id}", response_model=List[ContractStoreResponse])
+async def list_contracts(
+    store_id: int, 
+    templates_server: ContractServer = Depends(get_contract_service)
+) -> List[ContractStoreResponse]:
     """根据 store_id 查询所有合同"""
     rows = await templates_server.list_by_store(store_id)
-    return rows
+    return [ContractStoreResponse(**row) for row in rows]
 
-@router.get("/get_all_templates")
+@router.get("/get_all_templates", response_model=PaginatedResponse)
 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:
+) -> PaginatedResponse:
     """分页查询所有合同"""
-    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)):
+    rows, total = await templates_server.list_all_paged(page, page_size)
+    total_pages = (total + page_size - 1) // page_size if total > 0 else 0
+    
+    items = [ContractStoreResponse(**row) for row in rows]
+    return PaginatedResponse(
+        items=items,
+        total=total,
+        page=page,
+        page_size=page_size,
+        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签宝发起请求
@@ -127,18 +161,21 @@ async def get_esign_sign_url(body: SignUrl, templates_server: ContractServer = D
         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}
+        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 {"success": False, "message": "e签宝返回缺少签署链接", "raw": 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 {"success": True, "data": {"url": result_sign_url}}
+    return SignUrlResponse(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:
+@router.post("/esign/callback", response_model=Union[SuccessResponse, ErrorResponse])
+async def esign_callback(
+    payload: dict, 
+    templates_server: ContractServer = Depends(get_contract_service)
+) -> Union[SuccessResponse, ErrorResponse]:
     """
     e签宝签署结果回调
     需求:签署完成 -> 更新 signing_status=已签署,contract_url 中 status=1
@@ -170,12 +207,15 @@ async def esign_callback(payload: dict, templates_server: ContractServer = Depen
         
         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"}
+        return SuccessResponse(code="200", msg="success")
     logger.error(f"esign_callback ignored payload: {payload}")
-    return {"success": False, "message": "未处理: signResult!=2 或手机号/签署流程缺失"}
+    return ErrorResponse(success=False, message="未处理: signResult!=2 或手机号/签署流程缺失")
 
-@router.post("/esign/callback_auth")
-async def esign_callback_auth(payload: dict, templates_server: ContractServer = Depends(get_contract_service)):
+@router.post("/esign/callback_auth", response_model=SuccessResponse)
+async def esign_callback_auth(
+    payload: dict, 
+    templates_server: ContractServer = Depends(get_contract_service)
+) -> SuccessResponse:
     logger.info(f"esign_callback_auth payload: {payload}")
-    return {"code":"200","msg":"success"}
+    return SuccessResponse(code="200", msg="success")
 

+ 10 - 2
alien_store/repositories/contract_repo.py

@@ -24,12 +24,20 @@ class ContractRepository:
         return [dict(row) for row in result.mappings().all()]
 
     async def get_all_paged(self, page: int, page_size: int = 10):
-        """分页查询所有合同"""
+        """分页查询所有合同,返回 (items, total)"""
         offset = (page - 1) * page_size
+        # 查询总数
+        from sqlalchemy import func, select
+        count_result = await self.db.execute(
+            select(func.count()).select_from(ContractStore.__table__)
+        )
+        total = count_result.scalar() or 0
+        # 查询分页数据
         result = await self.db.execute(
             ContractStore.__table__.select().offset(offset).limit(page_size)
         )
-        return [dict(row) for row in result.mappings().all()]
+        items = [dict(row) for row in result.mappings().all()]
+        return items, total
 
     async def create(self, user_data):
         """创建未签署合同模板"""

+ 109 - 0
alien_store/schemas/response/contract_store.py

@@ -0,0 +1,109 @@
+from pydantic import BaseModel, Field
+from typing import Optional, List, Any
+from datetime import datetime
+
+
+class ContractItemResponse(BaseModel):
+    """合同项响应模型(contract_url 中的单个合同项)"""
+    contract_url: str = Field(description="合同模版链接")
+    file_name: str = Field(description="签署的合同的文件名称")
+    file_id: str = Field(description="生成的文件ID")
+    status: int = Field(description="签署状态 0 未签署 1 已签署")
+    sign_flow_id: str = Field(description="签署流程ID")
+    sign_url: str = Field(default="", description="e签宝生成的签署页面")
+    signing_time: str = Field(default="", description="签署合同的时间")
+    effective_time: str = Field(default="", description="合同生效的时间")
+    expiry_time: str = Field(default="", description="合同失效的时间")
+    contract_download_url: str = Field(default="", description="合同签署完成后下载文件的链接")
+    is_master: int = Field(description="是否是入驻店铺的协议合同 是 1 否 0")
+
+
+class ContractStoreResponse(BaseModel):
+    """合同记录响应模型"""
+    id: int = Field(description="主键")
+    store_id: int = Field(description="店铺id")
+    business_segment: str = Field(description="经营板块")
+    merchant_name: str = Field(description="商家姓名")
+    contact_phone: str = Field(description="联系电话")
+    signing_status: str = Field(description="签署状态(已签署,未签署,已到期)")
+    contract_url: str = Field(description="合同URL(JSON字符串)")
+    seal_url: str = Field(description="印章URL")
+    signing_time: Optional[datetime] = Field(default=None, description="签署时间")
+    effective_time: Optional[datetime] = Field(default=None, description="生效时间")
+    expiry_time: Optional[datetime] = Field(default=None, description="到期时间")
+    created_time: Optional[datetime] = Field(default=None, description="创建时间")
+    updated_time: Optional[datetime] = Field(default=None, description="更新时间")
+    
+    class Config:
+        from_attributes = True
+
+
+class ContractStoreDetailResponse(BaseModel):
+    """合同记录详情响应模型(包含解析后的 contract_url)"""
+    id: int = Field(description="主键")
+    store_id: int = Field(description="店铺id")
+    business_segment: str = Field(description="经营板块")
+    merchant_name: str = Field(description="商家姓名")
+    contact_phone: str = Field(description="联系电话")
+    signing_status: str = Field(description="签署状态(已签署,未签署,已到期)")
+    contract_items: List[ContractItemResponse] = Field(description="合同项列表(解析后的 contract_url)")
+    seal_url: str = Field(description="印章URL")
+    signing_time: Optional[datetime] = Field(default=None, description="签署时间")
+    effective_time: Optional[datetime] = Field(default=None, description="生效时间")
+    expiry_time: Optional[datetime] = Field(default=None, description="到期时间")
+    created_time: Optional[datetime] = Field(default=None, description="创建时间")
+    updated_time: Optional[datetime] = Field(default=None, description="更新时间")
+
+
+class TemplatesCreateResponse(BaseModel):
+    """创建模板响应模型"""
+    success: bool = Field(description="是否成功")
+    message: str = Field(description="响应消息")
+    sign_flow_id: Optional[str] = Field(default=None, description="签署流程ID")
+    file_id: Optional[str] = Field(default=None, description="文件ID")
+    contract_url: Optional[str] = Field(default=None, description="合同URL")
+
+
+class SignUrlResponse(BaseModel):
+    """获取签署URL响应模型"""
+    success: bool = Field(description="是否成功")
+    data: dict = Field(description="响应数据", default_factory=dict)
+    
+    class Config:
+        json_schema_extra = {
+            "example": {
+                "success": True,
+                "data": {
+                    "url": "https://example.com/sign"
+                }
+            }
+        }
+
+
+class ErrorResponse(BaseModel):
+    """错误响应模型"""
+    success: bool = Field(default=False, description="是否成功")
+    message: str = Field(description="错误消息")
+    raw: Optional[Any] = Field(default=None, description="原始错误数据")
+
+
+class SuccessResponse(BaseModel):
+    """通用成功响应模型"""
+    code: str = Field(description="响应代码")
+    msg: str = Field(description="响应消息")
+
+
+class PaginatedResponse(BaseModel):
+    """分页响应模型"""
+    items: List[ContractStoreResponse] = Field(description="数据列表")
+    total: int = Field(description="总记录数")
+    page: int = Field(description="当前页码")
+    page_size: int = Field(description="每页条数")
+    total_pages: int = Field(description="总页数")
+
+
+class ModuleStatusResponse(BaseModel):
+    """模块状态响应模型"""
+    module: str = Field(description="模块名称")
+    status: str = Field(description="状态")
+

+ 2 - 1
alien_store/services/contract_server.py

@@ -18,7 +18,8 @@ class ContractServer:
         return await self.esign_repo.get_by_store_id(store_id)
 
     async def list_all_paged(self, page: int, page_size: int = 10):
-        return await self.esign_repo.get_all_paged(page, page_size)
+        items, total = await self.esign_repo.get_all_paged(page, page_size)
+        return items, total
 
     async def mark_signed_by_phone(self, contact_phone: str, sign_flow_id: str, signing_time, contract_download_url):
         return await self.esign_repo.mark_signed_by_phone(contact_phone, sign_flow_id, signing_time, contract_download_url)