feat: restore source parity and harden agent runtime
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
#!/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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user