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

497 lines
18 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
文档生成服务层
"""
from typing import Optional, List, Tuple, Dict, Any
from datetime import datetime, date
from sqlalchemy import select, func, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
from app.base_service import BaseService
from online_dev.document_generator.model import DocumentTemplate, GeneratedDocument
from online_dev.document_generator.schema import (
DocumentTemplateCreate, DocumentTemplateUpdate
)
class DocumentTemplateService(BaseService[DocumentTemplate, DocumentTemplateCreate, DocumentTemplateUpdate]):
"""文档模板服务"""
model = DocumentTemplate
@classmethod
async def get_list(
cls,
db: AsyncSession,
page: int = 1,
page_size: int = 20,
application_id: Optional[str] = None,
category: Optional[str] = None,
status: Optional[str] = None,
form_code: Optional[str] = None,
workflow_code: Optional[str] = None,
keyword: Optional[str] = None,
is_builtin: Optional[bool] = None,
) -> Tuple[List[DocumentTemplate], int]:
"""获取模板列表"""
conditions = [cls.model.is_deleted == False]
# 子应用过滤:有 application_id 时过滤该应用,无则只返回主应用数据
if application_id:
conditions.append(cls.model.application_id == application_id)
else:
conditions.append(cls.model.application_id.is_(None))
if category:
conditions.append(cls.model.category == category)
if status:
conditions.append(cls.model.status == status)
if form_code:
conditions.append(cls.model.form_code == form_code)
if workflow_code:
conditions.append(cls.model.workflow_code == workflow_code)
if is_builtin is not None:
conditions.append(cls.model.is_builtin == is_builtin)
if keyword:
conditions.append(
or_(
cls.model.name.ilike(f"%{keyword}%"),
cls.model.code.ilike(f"%{keyword}%"),
cls.model.description.ilike(f"%{keyword}%"),
)
)
# 查询总数
count_stmt = select(func.count()).select_from(cls.model).where(and_(*conditions))
total = (await db.execute(count_stmt)).scalar() or 0
# 查询列表
stmt = (
select(cls.model)
.where(and_(*conditions))
.order_by(cls.model.sys_create_datetime.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
result = await db.execute(stmt)
items = list(result.scalars().all())
return items, total
@classmethod
async def get_by_code(cls, db: AsyncSession, code: str) -> Optional[DocumentTemplate]:
"""根据编码获取模板"""
stmt = select(cls.model).where(
and_(cls.model.code == code, cls.model.is_deleted == False)
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
@classmethod
async def get_by_workflow_code(cls, db: AsyncSession, workflow_code: str) -> List[DocumentTemplate]:
"""根据工作流编码获取绑定的已发布模板列表"""
stmt = select(cls.model).where(
and_(
cls.model.workflow_code == workflow_code,
cls.model.status == "published",
cls.model.is_deleted == False
)
).order_by(cls.model.sort, cls.model.sys_create_datetime)
result = await db.execute(stmt)
return list(result.scalars().all())
@classmethod
async def get_by_form_code(cls, db: AsyncSession, form_code: str) -> List[DocumentTemplate]:
"""根据表单编码获取绑定的已发布模板列表"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"Querying templates for form_code: {form_code}")
# 先查询所有绑定该表单的模板(不限状态)用于调试
debug_stmt = select(cls.model).where(
and_(
cls.model.form_code == form_code,
cls.model.is_deleted == False
)
)
debug_result = await db.execute(debug_stmt)
all_templates = list(debug_result.scalars().all())
logger.info(f"Found {len(all_templates)} templates (all statuses) for form_code {form_code}")
for t in all_templates:
logger.info(f" - Template: {t.name} (code={t.code}, status={t.status}, form_code={t.form_code})")
# 正式查询:只返回已发布的模板
stmt = select(cls.model).where(
and_(
cls.model.form_code == form_code,
cls.model.status == "published",
cls.model.is_deleted == False
)
).order_by(cls.model.sort, cls.model.sys_create_datetime)
result = await db.execute(stmt)
published_templates = list(result.scalars().all())
logger.info(f"Returning {len(published_templates)} published templates")
return published_templates
@classmethod
async def has_templates_by_form_code(cls, db: AsyncSession, form_code: str) -> bool:
"""检查表单是否绑定了已发布的单据模板"""
stmt = select(func.count()).select_from(cls.model).where(
and_(
cls.model.form_code == form_code,
cls.model.status == "published",
cls.model.is_deleted == False
)
)
result = await db.execute(stmt)
count = result.scalar() or 0
return count > 0
@classmethod
async def publish(cls, db: AsyncSession, template_id: str) -> Optional[DocumentTemplate]:
"""发布模板"""
template = await cls.get_by_id(db, template_id)
if not template:
return None
template.status = "published"
template.version += 1
await db.commit()
await db.refresh(template)
return template
@classmethod
async def unpublish(cls, db: AsyncSession, template_id: str) -> Optional[DocumentTemplate]:
"""取消发布"""
template = await cls.get_by_id(db, template_id)
if not template:
return None
template.status = "draft"
await db.commit()
await db.refresh(template)
return template
@classmethod
async def copy(cls, db: AsyncSession, template_id: str, new_code: str, new_name: str) -> Optional[DocumentTemplate]:
"""复制模板"""
template = await cls.get_by_id(db, template_id)
if not template:
return None
# 创建新模板
new_template = DocumentTemplate(
application_id=template.application_id,
name=new_name,
code=new_code,
category=template.category,
description=template.description,
form_code=template.form_code,
workflow_code=template.workflow_code,
template_type=template.template_type,
template_content=template.template_content,
template_css=template.template_css,
page_size=template.page_size,
page_orientation=template.page_orientation,
page_margin=template.page_margin,
watermark_enabled=template.watermark_enabled,
watermark_text=template.watermark_text,
watermark_type=template.watermark_type,
watermark_image_id=template.watermark_image_id,
watermark_opacity=template.watermark_opacity,
watermark_angle=template.watermark_angle,
seal_enabled=template.seal_enabled,
seal_positions=template.seal_positions,
header_enabled=template.header_enabled,
header_template=template.header_template,
footer_enabled=template.footer_enabled,
footer_template=template.footer_template,
show_page_number=template.show_page_number,
status="draft",
is_builtin=False,
version=1,
)
db.add(new_template)
await db.commit()
await db.refresh(new_template)
return new_template
@classmethod
async def get_categories(cls, db: AsyncSession, application_id: Optional[str] = None) -> List[Dict[str, Any]]:
"""获取模板分类统计"""
conditions = [cls.model.is_deleted == False]
# 子应用过滤
if application_id:
conditions.append(cls.model.application_id == application_id)
else:
conditions.append(cls.model.application_id.is_(None))
stmt = (
select(cls.model.category, func.count(cls.model.id).label("count"))
.where(and_(*conditions))
.group_by(cls.model.category)
)
result = await db.execute(stmt)
rows = result.all()
category_labels = {
"leave": "请假单",
"expense": "报销单",
"purchase": "采购单",
"contract": "合同",
"certificate": "证明",
"other": "其他",
}
return [
{
"value": row.category,
"label": category_labels.get(row.category, row.category),
"count": row.count,
}
for row in rows
]
@staticmethod
def _template_to_export_dict(template: DocumentTemplate) -> Dict[str, Any]:
"""将模板转为可导出的 JSON 结构"""
return {
"name": template.name,
"code": template.code,
"category": template.category or "other",
"description": template.description or "",
"form_code": template.form_code,
"workflow_code": template.workflow_code,
"template_type": template.template_type or "designer",
"template_content": template.template_content,
"template_css": template.template_css,
"page_size": template.page_size or "A4",
"page_orientation": template.page_orientation or "portrait",
"page_margin": template.page_margin,
"watermark_enabled": template.watermark_enabled or False,
"watermark_text": template.watermark_text,
"watermark_type": template.watermark_type or "text",
"watermark_image_id": template.watermark_image_id,
"watermark_opacity": template.watermark_opacity if template.watermark_opacity is not None else 30,
"watermark_angle": template.watermark_angle if template.watermark_angle is not None else -45,
"seal_enabled": template.seal_enabled or False,
"seal_positions": template.seal_positions,
"header_enabled": template.header_enabled or False,
"header_template": template.header_template,
"footer_enabled": template.footer_enabled or False,
"footer_template": template.footer_template,
"show_page_number": template.show_page_number if template.show_page_number is not None else True,
"calculation_rules": template.calculation_rules,
}
@classmethod
async def export_config(cls, db: AsyncSession, template_id: str) -> Dict[str, Any]:
"""导出单据模板配置"""
template = await cls.get_by_id(db, template_id)
if not template:
return None
return cls._template_to_export_dict(template)
@classmethod
async def check_import(cls, db: AsyncSession, code: str) -> Dict[str, Any]:
"""导入预检查:编码是否冲突"""
existing = await cls.get_by_code(db, code) if code else None
code_exists = existing is not None
return {
"code_exists": code_exists,
"can_import": not code_exists,
}
@classmethod
async def import_config(
cls,
db: AsyncSession,
data: Dict[str, Any],
) -> DocumentTemplate:
"""导入单据模板配置(创建新草稿模板)"""
if not data.get("name") or not data.get("code"):
raise ValueError("缺少必要字段: name 或 code")
existing = await cls.get_by_code(db, data["code"])
if existing:
raise ValueError(f"模板编码已存在: {data['code']}")
create_data = DocumentTemplateCreate(**data)
return await cls.create(db, create_data)
class GeneratedDocumentService(BaseService[GeneratedDocument, None, None]):
"""生成的文档服务"""
model = GeneratedDocument
@classmethod
async def get_list(
cls,
db: AsyncSession,
page: int = 1,
page_size: int = 20,
template_id: Optional[str] = None,
form_code: Optional[str] = None,
form_data_id: Optional[str] = None,
instance_id: Optional[str] = None,
generator_id: Optional[str] = None,
keyword: Optional[str] = None,
) -> Tuple[List[GeneratedDocument], int]:
"""获取文档列表"""
conditions = [cls.model.is_deleted == False]
if template_id:
conditions.append(cls.model.template_id == template_id)
if form_code:
conditions.append(cls.model.form_code == form_code)
if form_data_id:
conditions.append(cls.model.form_data_id == form_data_id)
if instance_id:
conditions.append(cls.model.instance_id == instance_id)
if generator_id:
conditions.append(cls.model.generator_id == generator_id)
if keyword:
conditions.append(
or_(
cls.model.document_name.ilike(f"%{keyword}%"),
cls.model.document_no.ilike(f"%{keyword}%"),
)
)
# 查询总数
count_stmt = select(func.count()).select_from(cls.model).where(and_(*conditions))
total = (await db.execute(count_stmt)).scalar() or 0
# 查询列表
stmt = (
select(cls.model)
.where(and_(*conditions))
.order_by(cls.model.sys_create_datetime.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
result = await db.execute(stmt)
items = list(result.scalars().all())
return items, total
@classmethod
async def get_by_instance_id(
cls,
db: AsyncSession,
instance_id: str,
) -> List[GeneratedDocument]:
"""根据流程实例ID获取文档列表"""
stmt = (
select(cls.model)
.where(
and_(
cls.model.instance_id == instance_id,
cls.model.is_deleted == False,
)
)
.order_by(cls.model.sys_create_datetime.desc())
)
result = await db.execute(stmt)
return list(result.scalars().all())
@classmethod
async def get_by_template_and_form_data(
cls,
db: AsyncSession,
template_id: str,
form_data_id: str,
) -> List[GeneratedDocument]:
"""根据模板ID和表单数据ID获取文档列表"""
stmt = (
select(cls.model)
.where(
and_(
cls.model.template_id == template_id,
cls.model.form_data_id == form_data_id,
cls.model.is_deleted == False,
)
)
.order_by(cls.model.sys_create_datetime.desc())
)
result = await db.execute(stmt)
return list(result.scalars().all())
@classmethod
async def create_document(
cls,
db: AsyncSession,
template: DocumentTemplate,
file_id: str,
file_size: int,
page_count: int,
document_name: str,
form_data_id: Optional[str] = None,
instance_id: Optional[str] = None,
generator_id: Optional[str] = None,
generator_name: Optional[str] = None,
generate_type: str = "manual",
) -> GeneratedDocument:
"""创建文档记录"""
# 生成文档编号
today = date.today()
document_no = f"DOC{today.strftime('%Y%m%d')}{datetime.now().strftime('%H%M%S%f')[:10]}"
document = GeneratedDocument(
template_id=template.id,
template_code=template.code,
template_name=template.name,
form_code=template.form_code,
form_data_id=form_data_id,
workflow_code=template.workflow_code,
instance_id=instance_id,
document_name=document_name,
document_no=document_no,
file_id=file_id,
file_size=file_size,
page_count=page_count,
generate_type=generate_type,
generator_id=generator_id,
generator_name=generator_name,
status="generated",
)
db.add(document)
await db.commit()
await db.refresh(document)
return document
@classmethod
async def increment_download_count(cls, db: AsyncSession, document_id: str) -> None:
"""增加下载次数"""
document = await cls.get_by_id(db, document_id)
if document:
document.download_count += 1
document.status = "downloaded"
await db.commit()
@classmethod
async def mark_sealed(
cls,
db: AsyncSession,
document_id: str,
seal_info: Dict[str, Any]
) -> Optional[GeneratedDocument]:
"""标记已盖章"""
document = await cls.get_by_id(db, document_id)
if not document:
return None
document.sealed = True
document.seal_info = seal_info
document.status = "sealed"
await db.commit()
await db.refresh(document)
return document