#!/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