Files
ai-agent-admin/backend-fastapi/online_dev/contract/service.py
T

737 lines
23 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
合同管理服务(异步版本)
"""
import secrets
import uuid
import logging
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import select, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from online_dev.contract.model import (
ContractInstance,
ContractLog,
ContractSignature,
ContractSignToken,
ContractTemplate,
)
logger = logging.getLogger(__name__)
class ContractTemplateService:
"""合同模板服务"""
@staticmethod
async def get_list(
db: AsyncSession,
page: int = 1,
page_size: int = 20,
name: str = None,
code: str = None,
category: str = None,
status: str = None,
) -> Dict[str, Any]:
"""获取模板列表"""
conditions = [ContractTemplate.is_deleted == False]
if name:
conditions.append(ContractTemplate.name.ilike(f"%{name}%"))
if code:
conditions.append(ContractTemplate.code.ilike(f"%{code}%"))
if category:
conditions.append(ContractTemplate.category == category)
if status:
conditions.append(ContractTemplate.status == status)
# 获取总数
count_stmt = select(func.count(ContractTemplate.id)).where(and_(*conditions))
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 获取列表
offset = (page - 1) * page_size
stmt = select(ContractTemplate).where(and_(*conditions)).order_by(
ContractTemplate.sort, ContractTemplate.sys_create_datetime.desc()
).offset(offset).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return {
"items": items,
"total": total,
"page": page,
"pageSize": page_size,
}
@staticmethod
async def get_by_id(db: AsyncSession, template_id: str) -> Optional[ContractTemplate]:
"""根据ID获取模板"""
stmt = select(ContractTemplate).where(
ContractTemplate.id == template_id,
ContractTemplate.is_deleted == False
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
async def get_by_code(db: AsyncSession, code: str) -> Optional[ContractTemplate]:
"""根据编码获取模板"""
stmt = select(ContractTemplate).where(
ContractTemplate.code == code,
ContractTemplate.is_deleted == False
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
async def create(
db: AsyncSession,
name: str,
code: str,
category: str = "",
description: str = "",
template_config: Dict = None,
creator_id: str = None,
) -> ContractTemplate:
"""创建模板"""
template = ContractTemplate(
name=name,
code=code,
category=category,
description=description,
template_config=template_config or {},
sys_creator_id=creator_id,
sys_modifier_id=creator_id,
)
db.add(template)
await db.commit()
await db.refresh(template)
return template
@staticmethod
async def update(
db: AsyncSession,
template: ContractTemplate,
name: str = None,
category: str = None,
description: str = None,
template_config: Dict = None,
thumbnail: str = None,
) -> ContractTemplate:
"""更新模板"""
if name is not None:
template.name = name
if category is not None:
template.category = category
if description is not None:
template.description = description
if template_config is not None:
template.template_config = template_config
if thumbnail is not None:
template.thumbnail = thumbnail
await db.commit()
await db.refresh(template)
return template
@staticmethod
async def delete(db: AsyncSession, template: ContractTemplate):
"""删除模板(软删除)"""
template.is_deleted = True
await db.commit()
@staticmethod
async def publish(db: AsyncSession, template: ContractTemplate) -> ContractTemplate:
"""发布模板"""
template.status = "published"
template.version += 1
await db.commit()
await db.refresh(template)
return template
@staticmethod
async def disable(db: AsyncSession, template: ContractTemplate) -> ContractTemplate:
"""停用模板"""
template.status = "disabled"
await db.commit()
await db.refresh(template)
return template
@staticmethod
async def copy(
db: AsyncSession,
template: ContractTemplate,
new_code: str,
new_name: str = None,
creator_id: str = None,
) -> ContractTemplate:
"""复制模板"""
new_template = ContractTemplate(
name=new_name or f"{template.name}_副本",
code=new_code,
category=template.category,
description=template.description,
template_config=template.template_config,
thumbnail=template.thumbnail,
status="draft",
version=1,
sys_creator_id=creator_id,
sys_modifier_id=creator_id,
)
db.add(new_template)
await db.commit()
await db.refresh(new_template)
return new_template
@staticmethod
async def get_categories(db: AsyncSession) -> List[str]:
"""获取所有分类"""
stmt = select(ContractTemplate.category).where(
ContractTemplate.is_deleted == False,
ContractTemplate.category != ""
).distinct()
result = await db.execute(stmt)
return [row[0] for row in result.fetchall()]
class ContractInstanceService:
"""合同实例服务"""
@staticmethod
def generate_contract_no() -> str:
"""生成合同编号"""
now = datetime.now()
prefix = now.strftime("JZ%Y%m%d")
suffix = uuid.uuid4().hex[:6].upper()
return f"{prefix}{suffix}"
@staticmethod
async def check_contract_no_exists(
db: AsyncSession,
contract_no: str,
exclude_id: str = None
) -> bool:
"""检查合同编号是否已存在"""
conditions = [
ContractInstance.contract_no == contract_no,
ContractInstance.is_deleted == False
]
if exclude_id:
conditions.append(ContractInstance.id != exclude_id)
stmt = select(func.count(ContractInstance.id)).where(and_(*conditions))
result = await db.execute(stmt)
count = result.scalar() or 0
return count > 0
@staticmethod
async def get_list(
db: AsyncSession,
page: int = 1,
page_size: int = 20,
title: str = None,
contract_no: str = None,
status: str = None,
template_id: str = None,
creator_id: str = None,
) -> Dict[str, Any]:
"""获取合同列表"""
conditions = [ContractInstance.is_deleted == False]
if title:
conditions.append(ContractInstance.title.ilike(f"%{title}%"))
if contract_no:
conditions.append(ContractInstance.contract_no.ilike(f"%{contract_no}%"))
if status:
conditions.append(ContractInstance.status == status)
if template_id:
conditions.append(ContractInstance.template_id == template_id)
if creator_id:
conditions.append(ContractInstance.creator_id == creator_id)
# 获取总数
count_stmt = select(func.count(ContractInstance.id)).where(and_(*conditions))
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 获取列表
offset = (page - 1) * page_size
stmt = select(ContractInstance).where(and_(*conditions)).order_by(
ContractInstance.sys_create_datetime.desc()
).offset(offset).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return {
"items": items,
"total": total,
"page": page,
"pageSize": page_size,
}
@staticmethod
async def get_by_id(db: AsyncSession, instance_id: str) -> Optional[ContractInstance]:
"""根据ID获取合同"""
stmt = select(ContractInstance).where(
ContractInstance.id == instance_id,
ContractInstance.is_deleted == False
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
async def create(
db: AsyncSession,
template_id: str,
title: str,
creator_id: str,
variable_data: Dict = None,
contract_no: str = None,
) -> ContractInstance:
"""创建合同实例"""
# 获取模板
template = await ContractTemplateService.get_by_id(db, template_id)
if not template:
raise ValueError(f"模板不存在: {template_id}")
# 如果没有提供合同编号,自动生成
if not contract_no:
contract_no = ContractInstanceService.generate_contract_no()
# 检查合同编号唯一性
if await ContractInstanceService.check_contract_no_exists(db, contract_no):
raise ValueError(f"合同编号 {contract_no} 已存在")
instance = ContractInstance(
template_id=template_id,
contract_no=contract_no,
title=title,
creator_id=creator_id,
contract_config=template.template_config,
variable_data=variable_data or {},
created_at=datetime.now(),
sys_creator_id=creator_id,
sys_modifier_id=creator_id,
)
db.add(instance)
await db.flush()
# 记录日志
log = ContractLog(
contract_id=instance.id,
action="create",
operator_id=creator_id,
sys_creator_id=creator_id,
)
db.add(log)
await db.commit()
await db.refresh(instance)
return instance
@staticmethod
async def update(
db: AsyncSession,
instance: ContractInstance,
title: str = None,
variable_data: Dict = None,
signature_data: Dict = None,
operator_id: str = None,
) -> ContractInstance:
"""更新合同"""
if title is not None:
instance.title = title
if variable_data is not None:
instance.variable_data = variable_data
if signature_data is not None:
instance.signature_data = signature_data
instance.sys_modifier_id = operator_id
# 记录日志
if operator_id:
log = ContractLog(
contract_id=instance.id,
action="update",
operator_id=operator_id,
sys_creator_id=operator_id,
)
db.add(log)
await db.commit()
await db.refresh(instance)
return instance
@staticmethod
async def submit(
db: AsyncSession,
instance: ContractInstance,
operator_id: str
) -> ContractInstance:
"""提交合同(进入待签署状态)"""
instance.status = "pending"
instance.sys_modifier_id = operator_id
log = ContractLog(
contract_id=instance.id,
action="submit",
operator_id=operator_id,
sys_creator_id=operator_id,
)
db.add(log)
await db.commit()
await db.refresh(instance)
return instance
@staticmethod
async def complete(
db: AsyncSession,
instance: ContractInstance,
operator_id: str
) -> ContractInstance:
"""完成合同"""
instance.status = "completed"
instance.completed_at = datetime.now()
instance.sys_modifier_id = operator_id
log = ContractLog(
contract_id=instance.id,
action="complete",
operator_id=operator_id,
sys_creator_id=operator_id,
)
db.add(log)
await db.commit()
await db.refresh(instance)
return instance
@staticmethod
async def cancel(
db: AsyncSession,
instance: ContractInstance,
operator_id: str,
comment: str = ""
) -> ContractInstance:
"""取消合同"""
instance.status = "canceled"
instance.sys_modifier_id = operator_id
log = ContractLog(
contract_id=instance.id,
action="cancel",
operator_id=operator_id,
comment=comment,
sys_creator_id=operator_id,
)
db.add(log)
await db.commit()
await db.refresh(instance)
return instance
@staticmethod
async def delete(db: AsyncSession, instance: ContractInstance):
"""删除合同(软删除)"""
instance.is_deleted = True
await db.commit()
@staticmethod
async def get_logs(db: AsyncSession, instance_id: str) -> List[ContractLog]:
"""获取合同日志"""
stmt = select(ContractLog).where(
ContractLog.contract_id == instance_id
).order_by(ContractLog.sys_create_datetime)
result = await db.execute(stmt)
return list(result.scalars().all())
class ContractSignatureService:
"""合同签署服务"""
@staticmethod
async def sign(
db: AsyncSession,
instance: ContractInstance,
element_id: str,
signature_image: str,
signer_id: str,
signer_name: str = "",
sign_ip: str = "",
sign_device: str = "",
) -> ContractSignature:
"""签署合同"""
# 查找元素信息
element = None
for el in instance.contract_config.get("elements", []):
if el.get("id") == element_id:
element = el
break
if not element:
raise ValueError("签署区域不存在")
# 获取签署方信息
signature_config = element.get("signature", {})
party_type = signature_config.get("partyType", "party_a")
party_label = signature_config.get("partyLabel", "")
sign_type = "seal" if element.get("type") == "seal-zone" else "signature"
# 查找是否已存在签署记录
stmt = select(ContractSignature).where(
ContractSignature.contract_id == instance.id,
ContractSignature.element_id == element_id
)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
# 更新现有记录
existing.party_type = party_type
existing.party_label = party_label
existing.sign_type = sign_type
existing.signer_id = signer_id
existing.signer_name = signer_name
existing.signature_image = signature_image
existing.status = "signed"
existing.signed_at = datetime.now()
existing.sign_ip = sign_ip
existing.sign_device = sign_device
signature = existing
else:
# 创建新记录
signature = ContractSignature(
contract_id=instance.id,
element_id=element_id,
party_type=party_type,
party_label=party_label,
sign_type=sign_type,
signer_id=signer_id,
signer_name=signer_name,
signature_image=signature_image,
status="signed",
signed_at=datetime.now(),
sign_ip=sign_ip,
sign_device=sign_device,
sys_creator_id=signer_id,
)
db.add(signature)
# 更新合同签署数据
signature_data = instance.signature_data or {}
signature_data[element_id] = signature_image
instance.signature_data = signature_data
# 检查是否所有必须签署区域都已签署
all_signed = await ContractSignatureService.check_all_signed(instance)
if all_signed and instance.status == "pending":
instance.status = "signing"
# 记录日志
log = ContractLog(
contract_id=instance.id,
action="sign",
operator_id=signer_id,
comment=f"{party_label}签署",
extra_data={"element_id": element_id},
ip_address=sign_ip,
sys_creator_id=signer_id,
)
db.add(log)
await db.commit()
await db.refresh(signature)
return signature
@staticmethod
async def check_all_signed(instance: ContractInstance) -> bool:
"""检查是否所有必须签署区域都已签署"""
elements = instance.contract_config.get("elements", [])
signature_data = instance.signature_data or {}
for element in elements:
if element.get("type") in ["signature-zone", "seal-zone"]:
signature_config = element.get("signature", {})
if signature_config.get("required", True):
if element.get("id") not in signature_data:
return False
return True
@staticmethod
async def get_signatures(db: AsyncSession, instance_id: str) -> List[ContractSignature]:
"""获取合同签署记录"""
stmt = select(ContractSignature).where(
ContractSignature.contract_id == instance_id
).order_by(ContractSignature.sys_create_datetime)
result = await db.execute(stmt)
return list(result.scalars().all())
class MobileSignService:
"""移动端签署服务"""
@staticmethod
async def create_sign_token(
db: AsyncSession,
instance: ContractInstance,
party_type: str = "party_a",
signer_name: str = "",
expire_minutes: int = 30,
) -> ContractSignToken:
"""创建签署令牌"""
# 生成随机令牌
token = secrets.token_urlsafe(32)
# 计算过期时间
expired_at = datetime.now() + timedelta(minutes=expire_minutes)
# 创建令牌记录
sign_token = ContractSignToken(
contract_id=instance.id,
token=token,
party_type=party_type,
signer_name=signer_name,
expired_at=expired_at,
)
db.add(sign_token)
await db.commit()
await db.refresh(sign_token)
return sign_token
@staticmethod
async def get_by_token(db: AsyncSession, token: str) -> Optional[ContractSignToken]:
"""根据令牌获取签署令牌"""
stmt = select(ContractSignToken).where(
ContractSignToken.token == token,
ContractSignToken.is_used == False
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
def validate_token(sign_token: ContractSignToken) -> Tuple[bool, str]:
"""验证令牌有效性"""
if sign_token.is_used:
return False, "该签署链接已被使用"
if sign_token.expired_at < datetime.now():
return False, "该签署链接已过期"
return True, ""
@staticmethod
async def mobile_sign(
db: AsyncSession,
sign_token: ContractSignToken,
instance: ContractInstance,
element_id: str,
signature_image: str,
signer_name: str = "",
sign_ip: str = "",
sign_device: str = "",
) -> ContractSignature:
"""移动端签署"""
# 查找元素信息
element = None
for el in instance.contract_config.get("elements", []):
if el.get("id") == element_id:
element = el
break
if not element:
raise ValueError("签署区域不存在")
# 获取签署方信息
signature_config = element.get("signature", {})
party_type = signature_config.get("partyType", "party_a")
party_label = signature_config.get("partyLabel", "")
sign_type = "seal" if element.get("type") == "seal-zone" else "signature"
# 使用令牌中的签署人姓名(如果提供)
final_signer_name = signer_name or sign_token.signer_name
# 截断设备信息,避免超出数据库字段长度
device_info = (sign_device + " (Mobile)")[:200] if sign_device else "Mobile"
# 查找是否已存在签署记录
stmt = select(ContractSignature).where(
ContractSignature.contract_id == instance.id,
ContractSignature.element_id == element_id
)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
# 更新现有记录
existing.party_type = party_type
existing.party_label = party_label
existing.sign_type = sign_type
existing.signer_id = None
existing.signer_name = final_signer_name
existing.signature_image = signature_image
existing.status = "signed"
existing.signed_at = datetime.now()
existing.sign_ip = sign_ip
existing.sign_device = device_info
signature = existing
else:
# 创建新记录
signature = ContractSignature(
contract_id=instance.id,
element_id=element_id,
party_type=party_type,
party_label=party_label,
sign_type=sign_type,
signer_id=None,
signer_name=final_signer_name,
signature_image=signature_image,
status="signed",
signed_at=datetime.now(),
sign_ip=sign_ip,
sign_device=device_info,
)
db.add(signature)
# 更新合同签署数据
signature_data = instance.signature_data or {}
signature_data[element_id] = signature_image
instance.signature_data = signature_data
# 检查是否所有必须签署区域都已签署
all_signed = await ContractSignatureService.check_all_signed(instance)
if all_signed and instance.status == "pending":
instance.status = "signing"
# 标记令牌已使用
sign_token.is_used = True
sign_token.used_at = datetime.now()
# 记录日志
log = ContractLog(
contract_id=instance.id,
action="sign",
operator_id=instance.creator_id,
comment=f"{party_label}通过手机签署 (签署人: {final_signer_name})",
extra_data={"element_id": element_id, "mobile": True},
ip_address=sign_ip,
sys_creator_id=instance.creator_id,
)
db.add(log)
await db.commit()
await db.refresh(signature)
return signature