774 lines
26 KiB
Python
774 lines
26 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
合同管理 API(异步版本)
|
|
"""
|
|
import logging
|
|
from typing import List
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.base_schema import PaginatedResponse
|
|
from online_dev.contract.schema import (
|
|
ContractInstanceCreate,
|
|
ContractInstanceListItem,
|
|
ContractInstanceOut,
|
|
ContractInstanceUpdate,
|
|
ContractLogOut,
|
|
ContractSignatureCreate,
|
|
ContractSignatureOut,
|
|
ContractTemplateCreate,
|
|
ContractTemplateListItem,
|
|
ContractTemplateOut,
|
|
ContractTemplateUpdate,
|
|
MobileContractInfo,
|
|
MobileSignRequest,
|
|
MobileSignTokenCreate,
|
|
MobileSignTokenOut,
|
|
)
|
|
from online_dev.contract.service import (
|
|
ContractInstanceService,
|
|
ContractSignatureService,
|
|
ContractTemplateService,
|
|
MobileSignService,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/contract", tags=["合同管理"])
|
|
|
|
|
|
# ============ 辅助函数 ============
|
|
|
|
def _format_datetime(dt) -> str:
|
|
"""格式化日期时间"""
|
|
if dt:
|
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
return ""
|
|
|
|
|
|
async def _build_template_out(template) -> dict:
|
|
"""构建模板输出"""
|
|
return {
|
|
"id": str(template.id),
|
|
"name": template.name,
|
|
"code": template.code,
|
|
"category": template.category or "",
|
|
"description": template.description or "",
|
|
"status": template.status,
|
|
"version": template.version,
|
|
"template_config": template.template_config or {},
|
|
"thumbnail": template.thumbnail or "",
|
|
"sys_create_datetime": template.sys_create_datetime,
|
|
"sys_update_datetime": template.sys_update_datetime,
|
|
}
|
|
|
|
|
|
async def _build_template_list_item(template) -> dict:
|
|
"""构建模板列表项"""
|
|
return {
|
|
"id": str(template.id),
|
|
"name": template.name,
|
|
"code": template.code,
|
|
"category": template.category or "",
|
|
"description": template.description or "",
|
|
"status": template.status,
|
|
"version": template.version,
|
|
"thumbnail": template.thumbnail or "",
|
|
"sys_create_datetime": template.sys_create_datetime,
|
|
"sys_update_datetime": template.sys_update_datetime,
|
|
}
|
|
|
|
|
|
async def _build_instance_out(db: AsyncSession, instance) -> dict:
|
|
"""构建合同实例输出"""
|
|
# 获取关联的模板信息
|
|
template = await ContractTemplateService.get_by_id(db, instance.template_id)
|
|
template_name = template.name if template else ""
|
|
template_code = template.code if template else ""
|
|
|
|
# 获取创建人信息
|
|
creator_name = ""
|
|
if instance.creator_id:
|
|
from core.user.model import User
|
|
from sqlalchemy import select
|
|
stmt = select(User).where(User.id == instance.creator_id)
|
|
result = await db.execute(stmt)
|
|
creator = result.scalar_one_or_none()
|
|
if creator:
|
|
creator_name = creator.name or creator.username or ""
|
|
|
|
return {
|
|
"id": str(instance.id),
|
|
"contract_no": instance.contract_no,
|
|
"title": instance.title,
|
|
"status": instance.status,
|
|
"template_id": str(instance.template_id),
|
|
"contract_config": instance.contract_config or {},
|
|
"variable_data": instance.variable_data or {},
|
|
"signature_data": instance.signature_data or {},
|
|
"created_at": instance.created_at,
|
|
"completed_at": instance.completed_at,
|
|
"expired_at": instance.expired_at,
|
|
"pdf_file": instance.pdf_file or "",
|
|
"template_name": template_name,
|
|
"template_code": template_code,
|
|
"creator_id": str(instance.creator_id) if instance.creator_id else "",
|
|
"creator_name": creator_name,
|
|
}
|
|
|
|
|
|
async def _build_instance_list_item(db: AsyncSession, instance) -> dict:
|
|
"""构建合同实例列表项"""
|
|
# 获取关联的模板信息
|
|
template = await ContractTemplateService.get_by_id(db, instance.template_id)
|
|
template_name = template.name if template else ""
|
|
|
|
# 获取创建人信息
|
|
creator_name = ""
|
|
if instance.creator_id:
|
|
from core.user.model import User
|
|
from sqlalchemy import select
|
|
stmt = select(User).where(User.id == instance.creator_id)
|
|
result = await db.execute(stmt)
|
|
creator = result.scalar_one_or_none()
|
|
if creator:
|
|
creator_name = creator.name or creator.username or ""
|
|
|
|
return {
|
|
"id": str(instance.id),
|
|
"contract_no": instance.contract_no,
|
|
"title": instance.title,
|
|
"status": instance.status,
|
|
"created_at": instance.created_at,
|
|
"completed_at": instance.completed_at,
|
|
"template_name": template_name,
|
|
"creator_name": creator_name,
|
|
}
|
|
|
|
|
|
def _build_signature_out(signature) -> dict:
|
|
"""构建签署记录输出"""
|
|
return {
|
|
"id": str(signature.id),
|
|
"element_id": signature.element_id,
|
|
"party_type": signature.party_type,
|
|
"party_label": signature.party_label or "",
|
|
"sign_type": signature.sign_type,
|
|
"signer_id": str(signature.signer_id) if signature.signer_id else None,
|
|
"signer_name": signature.signer_name or "",
|
|
"signature_image": signature.signature_image or "",
|
|
"status": signature.status,
|
|
"signed_at": signature.signed_at,
|
|
}
|
|
|
|
|
|
async def _build_log_out(db: AsyncSession, log) -> dict:
|
|
"""构建日志输出"""
|
|
# 获取操作人信息
|
|
operator_name = ""
|
|
if log.operator_id:
|
|
from core.user.model import User
|
|
from sqlalchemy import select
|
|
stmt = select(User).where(User.id == log.operator_id)
|
|
result = await db.execute(stmt)
|
|
operator = result.scalar_one_or_none()
|
|
if operator:
|
|
operator_name = operator.name or operator.username or ""
|
|
|
|
return {
|
|
"id": str(log.id),
|
|
"action": log.action,
|
|
"operator_id": str(log.operator_id) if log.operator_id else "",
|
|
"comment": log.comment or "",
|
|
"sys_create_datetime": log.sys_create_datetime,
|
|
"operator_name": operator_name,
|
|
}
|
|
|
|
|
|
# ============ 合同模板 API ============
|
|
|
|
@router.get("/template/list", response_model=PaginatedResponse[ContractTemplateListItem], summary="模板列表")
|
|
async def list_templates(
|
|
name: str = Query(None, description="模板名称"),
|
|
code: str = Query(None, description="模板编码"),
|
|
category: str = Query(None, description="分类"),
|
|
status: str = Query(None, description="状态"),
|
|
page: int = Query(default=1, ge=1, description="页码"),
|
|
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize", description="每页数量"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""分页查询合同模板列表"""
|
|
result = await ContractTemplateService.get_list(
|
|
db=db,
|
|
page=page,
|
|
page_size=page_size,
|
|
name=name,
|
|
code=code,
|
|
category=category,
|
|
status=status,
|
|
)
|
|
|
|
items = [await _build_template_list_item(item) for item in result["items"]]
|
|
|
|
return PaginatedResponse(
|
|
items=items,
|
|
total=result["total"],
|
|
)
|
|
|
|
|
|
@router.get("/template/categories", response_model=List[str], summary="获取模板分类")
|
|
async def get_template_categories(db: AsyncSession = Depends(get_db)):
|
|
"""获取所有模板分类"""
|
|
return await ContractTemplateService.get_categories(db)
|
|
|
|
|
|
@router.get("/template/{template_id}", response_model=ContractTemplateOut, summary="模板详情")
|
|
async def get_template(
|
|
template_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取模板详情"""
|
|
template = await ContractTemplateService.get_by_id(db, template_id)
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
return await _build_template_out(template)
|
|
|
|
|
|
@router.get("/template/code/{code}", response_model=ContractTemplateOut, summary="根据编码获取模板")
|
|
async def get_template_by_code(
|
|
code: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""根据编码获取模板"""
|
|
template = await ContractTemplateService.get_by_code(db, code)
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
return await _build_template_out(template)
|
|
|
|
|
|
@router.post("/template", response_model=ContractTemplateOut, summary="创建模板")
|
|
async def create_template(
|
|
request: Request,
|
|
data: ContractTemplateCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""创建合同模板"""
|
|
user_id = request.state.user_id
|
|
|
|
# 检查编码是否已存在
|
|
if await ContractTemplateService.get_by_code(db, data.code):
|
|
raise HTTPException(status_code=400, detail=f"模板编码 {data.code} 已存在")
|
|
|
|
try:
|
|
template = await ContractTemplateService.create(
|
|
db=db,
|
|
name=data.name,
|
|
code=data.code,
|
|
category=data.category,
|
|
description=data.description,
|
|
template_config=data.template_config,
|
|
creator_id=user_id,
|
|
)
|
|
return await _build_template_out(template)
|
|
except Exception as e:
|
|
logger.exception("创建模板失败")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.put("/template/{template_id}", response_model=ContractTemplateOut, summary="更新模板")
|
|
async def update_template(
|
|
template_id: str,
|
|
data: ContractTemplateUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""更新合同模板"""
|
|
template = await ContractTemplateService.get_by_id(db, template_id)
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
|
|
try:
|
|
template = await ContractTemplateService.update(
|
|
db=db,
|
|
template=template,
|
|
name=data.name,
|
|
category=data.category,
|
|
description=data.description,
|
|
template_config=data.template_config,
|
|
thumbnail=data.thumbnail,
|
|
)
|
|
return await _build_template_out(template)
|
|
except Exception as e:
|
|
logger.exception("更新模板失败")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.delete("/template/{template_id}", summary="删除模板")
|
|
async def delete_template(
|
|
template_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""删除合同模板"""
|
|
template = await ContractTemplateService.get_by_id(db, template_id)
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
|
|
await ContractTemplateService.delete(db, template)
|
|
return {"message": "删除成功"}
|
|
|
|
|
|
@router.delete("/template/batch", summary="批量删除模板")
|
|
async def batch_delete_templates(
|
|
ids: List[str] = Query(..., description="模板ID列表"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""批量删除合同模板"""
|
|
count = 0
|
|
for template_id in ids:
|
|
template = await ContractTemplateService.get_by_id(db, template_id)
|
|
if template:
|
|
await ContractTemplateService.delete(db, template)
|
|
count += 1
|
|
return {"count": count}
|
|
|
|
|
|
@router.post("/template/{template_id}/publish", response_model=ContractTemplateOut, summary="发布模板")
|
|
async def publish_template(
|
|
template_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""发布合同模板"""
|
|
template = await ContractTemplateService.get_by_id(db, template_id)
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
|
|
template = await ContractTemplateService.publish(db, template)
|
|
return await _build_template_out(template)
|
|
|
|
|
|
@router.post("/template/{template_id}/disable", response_model=ContractTemplateOut, summary="停用模板")
|
|
async def disable_template(
|
|
template_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""停用合同模板"""
|
|
template = await ContractTemplateService.get_by_id(db, template_id)
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
|
|
template = await ContractTemplateService.disable(db, template)
|
|
return await _build_template_out(template)
|
|
|
|
|
|
@router.post("/template/{template_id}/copy", response_model=ContractTemplateOut, summary="复制模板")
|
|
async def copy_template(
|
|
request: Request,
|
|
template_id: str,
|
|
new_code: str = Query(..., alias="newCode", description="新模板编码"),
|
|
new_name: str = Query(None, alias="newName", description="新模板名称"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""复制合同模板"""
|
|
user_id = request.state.user_id
|
|
|
|
template = await ContractTemplateService.get_by_id(db, template_id)
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="模板不存在")
|
|
|
|
# 检查新编码是否已存在
|
|
if await ContractTemplateService.get_by_code(db, new_code):
|
|
raise HTTPException(status_code=400, detail=f"模板编码 {new_code} 已存在")
|
|
|
|
new_template = await ContractTemplateService.copy(
|
|
db=db,
|
|
template=template,
|
|
new_code=new_code,
|
|
new_name=new_name,
|
|
creator_id=user_id,
|
|
)
|
|
return await _build_template_out(new_template)
|
|
|
|
|
|
# ============ 合同实例 API ============
|
|
|
|
@router.get("/instance/generate-no", summary="生成合同编号")
|
|
async def generate_contract_no():
|
|
"""生成一个新的合同编号"""
|
|
return {"contract_no": ContractInstanceService.generate_contract_no()}
|
|
|
|
|
|
@router.get("/instance/check-no", summary="检查合同编号唯一性")
|
|
async def check_contract_no(
|
|
contract_no: str = Query(..., alias="contractNo", description="合同编号"),
|
|
exclude_id: str = Query(None, alias="excludeId", description="排除的合同ID"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""检查合同编号是否已存在"""
|
|
exists = await ContractInstanceService.check_contract_no_exists(db, contract_no, exclude_id)
|
|
return {"exists": exists, "valid": not exists}
|
|
|
|
|
|
@router.get("/instance/list", response_model=PaginatedResponse[ContractInstanceListItem], summary="合同列表")
|
|
async def list_instances(
|
|
title: str = Query(None, description="合同标题"),
|
|
contract_no: str = Query(None, alias="contractNo", description="合同编号"),
|
|
status: str = Query(None, description="状态"),
|
|
template_id: str = Query(None, alias="templateId", description="模板ID"),
|
|
page: int = Query(default=1, ge=1, description="页码"),
|
|
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize", description="每页数量"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""分页查询合同实例列表"""
|
|
result = await ContractInstanceService.get_list(
|
|
db=db,
|
|
page=page,
|
|
page_size=page_size,
|
|
title=title,
|
|
contract_no=contract_no,
|
|
status=status,
|
|
template_id=template_id,
|
|
)
|
|
|
|
items = [await _build_instance_list_item(db, item) for item in result["items"]]
|
|
|
|
return PaginatedResponse(
|
|
items=items,
|
|
total=result["total"],
|
|
)
|
|
|
|
|
|
@router.get("/instance/my", response_model=PaginatedResponse[ContractInstanceListItem], summary="我的合同")
|
|
async def list_my_instances(
|
|
request: Request,
|
|
title: str = Query(None, description="合同标题"),
|
|
status: str = Query(None, description="状态"),
|
|
page: int = Query(default=1, ge=1, description="页码"),
|
|
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize", description="每页数量"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取我创建的合同"""
|
|
user_id = request.state.user_id
|
|
|
|
result = await ContractInstanceService.get_list(
|
|
db=db,
|
|
page=page,
|
|
page_size=page_size,
|
|
title=title,
|
|
status=status,
|
|
creator_id=user_id,
|
|
)
|
|
|
|
items = [await _build_instance_list_item(db, item) for item in result["items"]]
|
|
|
|
return PaginatedResponse(
|
|
items=items,
|
|
total=result["total"],
|
|
)
|
|
|
|
|
|
@router.get("/instance/{instance_id}", response_model=ContractInstanceOut, summary="合同详情")
|
|
async def get_instance(
|
|
instance_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取合同详情"""
|
|
instance = await ContractInstanceService.get_by_id(db, instance_id)
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail="合同不存在")
|
|
return await _build_instance_out(db, instance)
|
|
|
|
|
|
@router.post("/instance", response_model=ContractInstanceOut, summary="创建合同")
|
|
async def create_instance(
|
|
request: Request,
|
|
data: ContractInstanceCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""创建合同实例"""
|
|
user_id = request.state.user_id
|
|
|
|
try:
|
|
instance = await ContractInstanceService.create(
|
|
db=db,
|
|
template_id=data.template_id,
|
|
title=data.title,
|
|
creator_id=user_id,
|
|
variable_data=data.variable_data,
|
|
contract_no=data.contract_no,
|
|
)
|
|
return await _build_instance_out(db, instance)
|
|
except Exception as e:
|
|
logger.exception("创建合同失败")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.put("/instance/{instance_id}", response_model=ContractInstanceOut, summary="更新合同")
|
|
async def update_instance(
|
|
request: Request,
|
|
instance_id: str,
|
|
data: ContractInstanceUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""更新合同"""
|
|
user_id = request.state.user_id
|
|
|
|
instance = await ContractInstanceService.get_by_id(db, instance_id)
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail="合同不存在")
|
|
|
|
try:
|
|
instance = await ContractInstanceService.update(
|
|
db=db,
|
|
instance=instance,
|
|
title=data.title,
|
|
variable_data=data.variable_data,
|
|
signature_data=data.signature_data,
|
|
operator_id=user_id,
|
|
)
|
|
return await _build_instance_out(db, instance)
|
|
except Exception as e:
|
|
logger.exception("更新合同失败")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.delete("/instance/{instance_id}", summary="删除合同")
|
|
async def delete_instance(
|
|
instance_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""删除合同"""
|
|
instance = await ContractInstanceService.get_by_id(db, instance_id)
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail="合同不存在")
|
|
|
|
await ContractInstanceService.delete(db, instance)
|
|
return {"message": "删除成功"}
|
|
|
|
|
|
@router.post("/instance/{instance_id}/submit", response_model=ContractInstanceOut, summary="提交合同")
|
|
async def submit_instance(
|
|
request: Request,
|
|
instance_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""提交合同(进入待签署状态)"""
|
|
user_id = request.state.user_id
|
|
|
|
instance = await ContractInstanceService.get_by_id(db, instance_id)
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail="合同不存在")
|
|
|
|
instance = await ContractInstanceService.submit(db, instance, user_id)
|
|
return await _build_instance_out(db, instance)
|
|
|
|
|
|
@router.post("/instance/{instance_id}/complete", response_model=ContractInstanceOut, summary="完成合同")
|
|
async def complete_instance(
|
|
request: Request,
|
|
instance_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""完成合同"""
|
|
user_id = request.state.user_id
|
|
|
|
instance = await ContractInstanceService.get_by_id(db, instance_id)
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail="合同不存在")
|
|
|
|
instance = await ContractInstanceService.complete(db, instance, user_id)
|
|
return await _build_instance_out(db, instance)
|
|
|
|
|
|
@router.post("/instance/{instance_id}/cancel", summary="取消合同")
|
|
async def cancel_instance(
|
|
request: Request,
|
|
instance_id: str,
|
|
comment: str = Query("", description="取消原因"),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""取消合同"""
|
|
user_id = request.state.user_id
|
|
|
|
instance = await ContractInstanceService.get_by_id(db, instance_id)
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail="合同不存在")
|
|
|
|
await ContractInstanceService.cancel(db, instance, user_id, comment)
|
|
return {"message": "取消成功"}
|
|
|
|
|
|
@router.get("/instance/{instance_id}/logs", response_model=List[ContractLogOut], summary="合同日志")
|
|
async def get_instance_logs(
|
|
instance_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取合同操作日志"""
|
|
logs = await ContractInstanceService.get_logs(db, instance_id)
|
|
return [await _build_log_out(db, log) for log in logs]
|
|
|
|
|
|
# ============ 签署 API ============
|
|
|
|
@router.post("/instance/{instance_id}/sign", response_model=ContractSignatureOut, summary="签署合同")
|
|
async def sign_instance(
|
|
request: Request,
|
|
instance_id: str,
|
|
data: ContractSignatureCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""签署合同"""
|
|
user_id = request.state.user_id
|
|
user_name = getattr(request.state, "username", "")
|
|
|
|
instance = await ContractInstanceService.get_by_id(db, instance_id)
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail="合同不存在")
|
|
|
|
# 获取客户端信息
|
|
sign_ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else "")
|
|
sign_device = request.headers.get("User-Agent", "")
|
|
|
|
try:
|
|
signature = await ContractSignatureService.sign(
|
|
db=db,
|
|
instance=instance,
|
|
element_id=data.element_id,
|
|
signature_image=data.signature_image,
|
|
signer_id=user_id,
|
|
signer_name=data.signer_name or user_name,
|
|
sign_ip=sign_ip,
|
|
sign_device=sign_device,
|
|
)
|
|
return _build_signature_out(signature)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.get("/instance/{instance_id}/signatures", response_model=List[ContractSignatureOut], summary="签署记录")
|
|
async def get_instance_signatures(
|
|
instance_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取合同签署记录"""
|
|
signatures = await ContractSignatureService.get_signatures(db, instance_id)
|
|
return [_build_signature_out(sig) for sig in signatures]
|
|
|
|
|
|
# ============ 手机签署 API ============
|
|
|
|
@router.post("/instance/{instance_id}/mobile-sign-token", response_model=MobileSignTokenOut, summary="生成手机签署二维码")
|
|
async def create_mobile_sign_token(
|
|
instance_id: str,
|
|
data: MobileSignTokenCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""生成手机签署令牌和二维码"""
|
|
instance = await ContractInstanceService.get_by_id(db, instance_id)
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail="合同不存在")
|
|
|
|
if instance.status not in ["pending", "signing"]:
|
|
raise HTTPException(status_code=400, detail="合同状态不允许签署")
|
|
|
|
try:
|
|
sign_token = await MobileSignService.create_sign_token(
|
|
db=db,
|
|
instance=instance,
|
|
party_type=data.party_type,
|
|
signer_name=data.signer_name,
|
|
expire_minutes=data.expire_minutes,
|
|
)
|
|
|
|
# 只返回 token,由前端构建完整 URL
|
|
return {
|
|
"token": sign_token.token,
|
|
"sign_url": "", # 由前端构建
|
|
"expired_at": sign_token.expired_at,
|
|
"qrcode_data": "", # 由前端构建
|
|
}
|
|
except Exception as e:
|
|
logger.exception("生成签署令牌失败")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.get("/mobile/contract/{token}", response_model=MobileContractInfo, summary="获取移动端合同信息")
|
|
async def get_mobile_contract(
|
|
token: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""根据令牌获取合同信息(无需登录)"""
|
|
sign_token = await MobileSignService.get_by_token(db, token)
|
|
if not sign_token:
|
|
raise HTTPException(status_code=404, detail="签署链接无效或已过期")
|
|
|
|
# 验证令牌
|
|
is_valid, error_msg = MobileSignService.validate_token(sign_token)
|
|
if not is_valid:
|
|
raise HTTPException(status_code=400, detail=error_msg)
|
|
|
|
# 获取合同
|
|
contract = await ContractInstanceService.get_by_id(db, sign_token.contract_id)
|
|
if not contract:
|
|
raise HTTPException(status_code=404, detail="合同不存在")
|
|
|
|
# 获取模板名称
|
|
template = await ContractTemplateService.get_by_id(db, contract.template_id)
|
|
template_name = template.name if template else ""
|
|
|
|
return {
|
|
"id": str(contract.id),
|
|
"contract_no": contract.contract_no,
|
|
"title": contract.title,
|
|
"status": contract.status,
|
|
"contract_config": contract.contract_config or {},
|
|
"variable_data": contract.variable_data or {},
|
|
"signature_data": contract.signature_data or {},
|
|
"party_type": sign_token.party_type,
|
|
"signer_name": sign_token.signer_name or "",
|
|
"template_name": template_name,
|
|
}
|
|
|
|
|
|
@router.post("/mobile/sign/{token}", response_model=ContractSignatureOut, summary="移动端签署")
|
|
async def mobile_sign(
|
|
request: Request,
|
|
token: str,
|
|
data: MobileSignRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""移动端签署合同(无需登录)"""
|
|
sign_token = await MobileSignService.get_by_token(db, token)
|
|
if not sign_token:
|
|
raise HTTPException(status_code=404, detail="签署链接无效或已过期")
|
|
|
|
# 验证令牌
|
|
is_valid, error_msg = MobileSignService.validate_token(sign_token)
|
|
if not is_valid:
|
|
raise HTTPException(status_code=400, detail=error_msg)
|
|
|
|
# 获取合同
|
|
instance = await ContractInstanceService.get_by_id(db, sign_token.contract_id)
|
|
if not instance:
|
|
raise HTTPException(status_code=404, detail="合同不存在")
|
|
|
|
# 获取客户端信息
|
|
sign_ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else "")
|
|
sign_device = request.headers.get("User-Agent", "")
|
|
|
|
try:
|
|
signature = await MobileSignService.mobile_sign(
|
|
db=db,
|
|
sign_token=sign_token,
|
|
instance=instance,
|
|
element_id=data.element_id,
|
|
signature_image=data.signature_image,
|
|
signer_name=data.signer_name,
|
|
sign_ip=sign_ip,
|
|
sign_device=sign_device,
|
|
)
|
|
return _build_signature_out(signature)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|