#!/usr/bin/env python # -*- coding: utf-8 -*- """ 工作流 API 接口 """ import json import logging from typing import List from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import StreamingResponse logger = logging.getLogger(__name__) from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from core.application.model import Application from app.config import settings from app.base_schema import PaginatedResponse, ResponseModel from online_dev.workflow.schema import ( WorkflowDefinitionCreate, WorkflowDefinitionUpdate, WorkflowDefinitionOut, WorkflowDefinitionListOut, WorkflowImportCheckIn, WorkflowImportCheckOut, WorkflowImportIn, WorkflowInstanceCreate, WorkflowInstanceOut, WorkflowInstanceListOut, WorkflowTaskOut, WorkflowTaskListOut, WorkflowLogOut, TaskApproveInput, TaskHandleInput, TaskReviseInput, TaskTransferInput, TaskDelegateInput, TaskAddSignInput, MessageResponse, CountResponse, ) from online_dev.workflow.service import ( WorkflowDefinitionService, WorkflowInstanceService, WorkflowTaskService, WorkflowLogService, ) router = APIRouter(prefix="/workflow", tags=["工作流管理"]) # ==================== 辅助函数 ==================== def _get_action_label(action: str) -> str: """获取操作标签""" labels = { 'approve': '通过', 'reject': '拒绝', 'return': '驳回', 'transfer': '转办', 'delegate': '委派', 'add_sign': '加签', 'reduce_sign': '减签', } return labels.get(action, action) async def _build_definition_out(db: AsyncSession, workflow) -> dict: """构建流程定义输出""" return { "id": str(workflow.id), "application_id": workflow.application_id, "name": workflow.name, "code": workflow.code, "workflow_type": workflow.workflow_type or "other", "icon": workflow.icon or "", "icon_bg_color": workflow.icon_bg_color or "", "category": workflow.category or "", "description": workflow.description or "", "status": workflow.status, "version": workflow.version, "form_code": workflow.form_code, "form_name": workflow.form_name or "", "flow_definition": workflow.flow_definition or {}, "sort": workflow.sort or 0, "created_at": workflow.sys_create_datetime, "updated_at": workflow.sys_update_datetime, } def _build_definition_list_out(workflow, application_name: str = "") -> dict: """构建流程定义列表输出""" return { "id": str(workflow.id), "application_id": workflow.application_id, "application_name": application_name, "name": workflow.name, "code": workflow.code, "workflow_type": workflow.workflow_type or "other", "icon": workflow.icon or "", "icon_bg_color": workflow.icon_bg_color or "", "category": workflow.category or "", "description": workflow.description or "", "status": workflow.status, "version": workflow.version, "form_code": workflow.form_code, "form_name": workflow.form_name or "", "sort": workflow.sort or 0, "created_at": workflow.sys_create_datetime, "updated_at": workflow.sys_update_datetime, } async def _build_instance_out(db: AsyncSession, instance) -> dict: """构建流程实例输出""" from online_dev.workflow.model import WorkflowDefinition from core.user.model import User from sqlalchemy import select # 获取流程定义 stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id) result = await db.execute(stmt) workflow = result.scalar_one_or_none() # 获取发起人 stmt = select(User).where(User.id == instance.initiator_id) result = await db.execute(stmt) initiator = result.scalar_one_or_none() # 判断流程是否绑定了单据模板(通过 DocumentTemplate.workflow_code 查找) has_document_templates = False if workflow: from online_dev.document_generator.service import DocumentTemplateService bound_templates = await DocumentTemplateService.get_by_workflow_code(db, workflow.code) has_document_templates = len(bound_templates) > 0 return { "id": str(instance.id), "instance_no": instance.instance_no, "title": instance.title, "status": instance.status, "workflow_id": str(instance.workflow_id), "workflow_name": workflow.name if workflow else "", "workflow_code": workflow.code if workflow else "", "form_code": instance.form_code, "form_data_id": instance.form_data_id, "current_node_id": instance.current_node_id or "", "current_node_name": instance.current_node_name or "", "initiator_id": str(instance.initiator_id), "initiator_name": initiator.name if initiator else "", "started_at": instance.started_at, "completed_at": instance.completed_at, "has_document_templates": has_document_templates, } async def _build_instance_list_out(db: AsyncSession, instance) -> dict: """构建流程实例列表输出""" from online_dev.workflow.model import WorkflowDefinition from core.user.model import User from sqlalchemy import select # 获取流程定义 stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id) result = await db.execute(stmt) workflow = result.scalar_one_or_none() # 获取发起人 stmt = select(User).where(User.id == instance.initiator_id) result = await db.execute(stmt) initiator = result.scalar_one_or_none() # 判断流程是否绑定了单据模板(通过 DocumentTemplate.workflow_code 查找) has_document_templates = False application_name = "" if workflow: from online_dev.document_generator.service import DocumentTemplateService bound_templates = await DocumentTemplateService.get_by_workflow_code(db, workflow.code) has_document_templates = len(bound_templates) > 0 if workflow.application_id: from core.application.model import Application app_stmt = select(Application).where(Application.id == workflow.application_id) app_result = await db.execute(app_stmt) app = app_result.scalar_one_or_none() application_name = app.name if app else "" return { "id": str(instance.id), "instance_no": instance.instance_no, "title": instance.title, "status": instance.status, "workflow_name": workflow.name if workflow else "", "application_name": application_name, "current_node_name": instance.current_node_name or "", "initiator_name": initiator.name if initiator else "", "started_at": instance.started_at, "completed_at": instance.completed_at, "has_document_templates": has_document_templates, } async def _build_task_out(db: AsyncSession, task, include_form: bool = False) -> dict: """构建任务输出""" from online_dev.workflow.model import WorkflowInstance, WorkflowDefinition, WorkflowLog from core.user.model import User from sqlalchemy import select # 获取实例 stmt = select(WorkflowInstance).where(WorkflowInstance.id == task.instance_id) result = await db.execute(stmt) instance = result.scalar_one_or_none() # 获取流程定义 workflow = None if instance: stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id) result = await db.execute(stmt) workflow = result.scalar_one_or_none() # 缓存 workflow 属性,避免后续 service.get() 内部 db.rollback() 导致 session 对象过期 _workflow_name = workflow.name if workflow else "" _flow_definition = workflow.flow_definition if workflow else {} # 获取处理人 stmt = select(User).where(User.id == task.assignee_id) result = await db.execute(stmt) assignee = result.scalar_one_or_none() # 获取发起人 initiator = None if instance: stmt = select(User).where(User.id == instance.initiator_id) result = await db.execute(stmt) initiator = result.scalar_one_or_none() # 判断取消原因 cancel_reason = "" if task.status == 'canceled': if (task.comment or '') == '__or_sign_canceled__': cancel_reason = "or_sign" else: cancel_reason = "revoked" # 判断流程是否绑定了单据模板 has_document_templates = False if workflow: from online_dev.document_generator.service import DocumentTemplateService bound_templates = await DocumentTemplateService.get_by_workflow_code(db, workflow.code) has_document_templates = len(bound_templates) > 0 out = { "id": str(task.id), "instance_id": str(task.instance_id), "instance_no": instance.instance_no if instance else "", "instance_title": instance.title if instance else "", "instance_status": instance.status if instance else "", "node_id": task.node_id, "node_name": task.node_name, "task_type": task.task_type, "status": task.status, "assignee_id": str(task.assignee_id), "assignee_name": assignee.name if assignee else "", "comment": "" if cancel_reason == "or_sign" else (task.comment or ""), "cancel_reason": cancel_reason, "handled_at": task.handled_at, "sys_create_datetime": task.sys_create_datetime, "workflow_name": _workflow_name, "initiator_name": initiator.name if initiator else "", "started_at": instance.started_at if instance else None, "has_document_templates": has_document_templates, } if include_form: form_code = instance.form_code if instance else None form_data_id = instance.form_data_id if instance else None out["form_code"] = form_code # 获取表单元数据(form_config) form_config = None if form_code: try: from online_dev.form_manager.model import FormMeta stmt = select(FormMeta).where( FormMeta.code == form_code, FormMeta.is_deleted == False, ) result = await db.execute(stmt) form_meta = result.scalar_one_or_none() if form_meta: form_config = form_meta.form_config except Exception as e: import logging logging.warning(f"获取表单配置失败: {e}") out["form_config"] = form_config # 获取表单数据(form_data) form_data = {} if form_code and form_data_id: try: from online_dev.form_data_manager.service import FormDataService service = await FormDataService.create_service(db, form_code) form_data = await service.get(db, form_data_id) except Exception as e: import logging logging.warning(f"获取表单数据失败: {e}") out["form_data"] = form_data # 获取当前节点的字段权限和操作权限 form_permissions = [] action_permissions = [] flow_def = _flow_definition nodes = flow_def.get('nodes', {}) # 递归查找节点 def find_node(node, target_id): if not node: return None if isinstance(node, dict): if node.get('id') == target_id: return node children = node.get('children') if children: found = find_node(children, target_id) if found: return found branches = node.get('branches', []) for branch in branches: if branch.get('id') == target_id: return branch branch_children = branch.get('children') if branch_children: found = find_node(branch_children, target_id) if found: return found elif isinstance(node, list): for item in node: found = find_node(item, target_id) if found: return found return None current_node = find_node(nodes, task.node_id) if current_node: node_config = current_node.get('config', {}) form_permissions = node_config.get('formPermissions', []) action_permissions = node_config.get('actionPermissions', []) # 如果没有新格式的配置,从旧的 actions 数组生成 if not action_permissions: actions = node_config.get('actions', ['approve', 'reject']) action_permissions = [ {'action': a, 'enabled': True, 'label': _get_action_label(a)} for a in actions ] # 默认操作权限 if not action_permissions: action_permissions = [ {'action': 'approve', 'enabled': True, 'label': '通过'}, {'action': 'reject', 'enabled': True, 'label': '拒绝'}, ] out["form_permissions"] = form_permissions out["action_permissions"] = action_permissions # 获取签名配置 require_signature = False if current_node: node_config = current_node.get('config', {}) require_signature = node_config.get('requireSignature', False) out["require_signature"] = require_signature # 获取最后一条驳回日志 last_return_log = None if task.task_type == 'revise': stmt = select(WorkflowLog).where( WorkflowLog.instance_id == task.instance_id, WorkflowLog.action == 'return', ).order_by(WorkflowLog.sys_create_datetime.desc()).limit(1) result = await db.execute(stmt) log = result.scalar_one_or_none() if log: # 获取操作人 stmt = select(User).where(User.id == log.operator_id) result = await db.execute(stmt) operator = result.scalar_one_or_none() last_return_log = { "operator_name": operator.name if operator else "", "comment": log.comment or "", "created_at": log.sys_create_datetime, } out["last_return_log"] = last_return_log return out async def _build_task_list_out(db: AsyncSession, task) -> dict: """构建任务列表输出""" from online_dev.workflow.model import WorkflowInstance, WorkflowDefinition from core.user.model import User from sqlalchemy import select from datetime import datetime # 获取实例 stmt = select(WorkflowInstance).where(WorkflowInstance.id == task.instance_id) result = await db.execute(stmt) instance = result.scalar_one_or_none() # 获取流程定义 workflow = None if instance: stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id) result = await db.execute(stmt) workflow = result.scalar_one_or_none() # 获取处理人 stmt = select(User).where(User.id == task.assignee_id) result = await db.execute(stmt) assignee = result.scalar_one_or_none() # 获取发起人 initiator = None if instance: stmt = select(User).where(User.id == instance.initiator_id) result = await db.execute(stmt) initiator = result.scalar_one_or_none() # 计算是否超时 is_timeout = False if task.timeout_at and task.status == 'pending': is_timeout = datetime.now() > task.timeout_at # 判断流程是否绑定了单据模板 has_document_templates = False if workflow: from online_dev.document_generator.service import DocumentTemplateService bound_templates = await DocumentTemplateService.get_by_workflow_code(db, workflow.code) has_document_templates = len(bound_templates) > 0 return { "id": str(task.id), "instance_id": str(task.instance_id), "instance_no": instance.instance_no if instance else "", "instance_title": instance.title if instance else "", "instance_status": instance.status if instance else "", "node_name": task.node_name, "task_type": task.task_type, "status": task.status, "assignee_id": str(task.assignee_id), "assignee_name": assignee.name if assignee else "", "workflow_name": workflow.name if workflow else "", "initiator_name": initiator.name if initiator else "", "started_at": instance.started_at if instance else None, "sys_create_datetime": task.sys_create_datetime, "has_document_templates": has_document_templates, "timeout_at": task.timeout_at, "timeout_action": task.timeout_action or "", "is_timeout": is_timeout, "workflow_icon": workflow.icon if workflow else "", "workflow_icon_bg_color": workflow.icon_bg_color if workflow else "", } async def _build_log_out(db: AsyncSession, log) -> dict: """构建日志输出""" 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() return { "id": str(log.id), "node_id": log.node_id or "", "node_name": log.node_name or "", "action": log.action, "operator_id": str(log.operator_id), "operator_name": operator.name if operator else "", "comment": log.comment or "", "extra_data": log.extra_data or {}, "sys_create_datetime": log.sys_create_datetime, } # ==================== 流程定义 API ==================== @router.get("/list", response_model=PaginatedResponse[WorkflowDefinitionListOut], summary="流程定义列表") async def get_definitions( page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=1000, alias="pageSize", description="每页数量"), application_id: str = Query(default=None, alias="applicationId", description="所属应用ID"), all_apps: bool = Query(default=False, alias="allApps", description="是否返回所有应用的流程"), name: str = Query(default=None, description="流程名称"), code: str = Query(default=None, description="流程编码"), workflow_type: str = Query(default=None, alias="workflowType", description="流程类型"), category: str = Query(default=None, description="分类"), status: str = Query(default=None, description="状态"), form_code: str = Query(default=None, alias="formCode", description="表单编码"), db: AsyncSession = Depends(get_db), ): """获取流程定义列表(自动应用数据权限)""" items, total = await WorkflowDefinitionService.get_list_with_data_scope( db, page=page, page_size=page_size, application_id=application_id, all_apps=all_apps, name=name, code=code, workflow_type=workflow_type, category=category, status=status, form_code=form_code, ) # 批量查询应用名称 app_ids = list({item.application_id for item in items if item.application_id}) app_name_map = {} if app_ids: app_result = await db.execute( select(Application.id, Application.name).where(Application.id.in_(app_ids)) ) app_name_map = {row.id: row.name for row in app_result} result_items = [_build_definition_list_out(item, app_name_map.get(item.application_id, "")) for item in items] return PaginatedResponse(items=result_items, total=total) @router.post("", response_model=WorkflowDefinitionOut, summary="创建流程") async def create_definition( request: Request, data: WorkflowDefinitionCreate, db: AsyncSession = Depends(get_db), ): """创建流程定义""" # 检查编码唯一性 existing = await WorkflowDefinitionService.get_by_code(db, data.code) if existing: raise HTTPException(status_code=400, detail="流程编码已存在") user_id = request.state.user_id workflow = await WorkflowDefinitionService.create(db, data.model_dump(), user_id) await db.commit() return await _build_definition_out(db, workflow) @router.get("/categories", summary="获取分类列表") async def get_definition_categories(db: AsyncSession = Depends(get_db)): """获取所有流程分类""" categories = await WorkflowDefinitionService.get_categories(db) return {"items": categories} @router.get("/available-forms", summary="获取可关联的表单列表") async def get_available_forms( form_type: str = Query(default="workflow", description="表单类型,默认只获取流程表单"), application_id: str = Query(None, alias="applicationId", description="所属应用ID"), db: AsyncSession = Depends(get_db), ): """获取已发布的表单列表供流程关联(按应用隔离,含全局可见跨应用表单)""" from online_dev.form_manager.model import FormMeta from online_dev.form_manager.service import FormService conditions = [ FormMeta.status == 'published', FormMeta.is_deleted == False, ] conditions.append( FormService.build_app_scope_condition( application_id, for_selection=True, ) ) if form_type: conditions.append(FormMeta.form_type == form_type) stmt = ( select( FormMeta.code, FormMeta.name, FormMeta.application_id, Application.name.label("application_name"), ) .outerjoin(Application, Application.id == FormMeta.application_id) .where(*conditions) .order_by(FormMeta.name) ) result = await db.execute(stmt) forms = [ { "code": row[0], "name": row[1], "application_id": row[2], "application_name": row[3] or "主应用", } for row in result.all() ] return forms @router.get("/by-form/{form_code}", response_model=List[WorkflowDefinitionListOut], summary="获取表单关联的流程") async def get_workflows_by_form( form_code: str, db: AsyncSession = Depends(get_db), ): """获取关联指定表单的已发布流程列表""" from online_dev.workflow.model import WorkflowDefinition from sqlalchemy import select stmt = select(WorkflowDefinition).where( WorkflowDefinition.form_code == form_code, WorkflowDefinition.status == 'published', WorkflowDefinition.is_deleted == False, ) result = await db.execute(stmt) workflows = result.scalars().all() return [_build_definition_list_out(w) for w in workflows] @router.get("/code/{code}", response_model=WorkflowDefinitionOut, summary="根据编码获取流程") async def get_definition_by_code( code: str, db: AsyncSession = Depends(get_db), ): """根据编码获取流程定义""" workflow = await WorkflowDefinitionService.get_by_code(db, code) if not workflow: raise HTTPException(status_code=404, detail="流程不存在") return await _build_definition_out(db, workflow) @router.get("/{workflow_id}", response_model=WorkflowDefinitionOut, summary="流程定义详情") async def get_definition( workflow_id: str, db: AsyncSession = Depends(get_db), ): """获取流程定义详情""" workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id) if not workflow: raise HTTPException(status_code=404, detail="流程定义不存在") return await _build_definition_out(db, workflow) @router.put("/{workflow_id}", response_model=WorkflowDefinitionOut, summary="更新流程") async def update_definition( request: Request, workflow_id: str, data: WorkflowDefinitionUpdate, db: AsyncSession = Depends(get_db), ): """更新流程定义""" user_id = request.state.user_id workflow = await WorkflowDefinitionService.update(db, workflow_id, data.model_dump(exclude_unset=True), user_id) if not workflow: raise HTTPException(status_code=404, detail="流程定义不存在") await db.commit() return await _build_definition_out(db, workflow) @router.delete("/{workflow_id}", response_model=ResponseModel, summary="删除流程") async def delete_definition( workflow_id: str, db: AsyncSession = Depends(get_db), ): """删除流程定义""" success = await WorkflowDefinitionService.delete(db, workflow_id) if not success: raise HTTPException(status_code=404, detail="流程定义不存在") await db.commit() return ResponseModel(message="删除成功") @router.delete("/batch/delete", response_model=CountResponse, summary="批量删除流程") async def batch_delete_definitions( ids: List[str] = Query(..., description="要删除的ID列表"), db: AsyncSession = Depends(get_db), ): """批量删除流程定义""" count = await WorkflowDefinitionService.batch_delete(db, ids) await db.commit() return CountResponse(count=count) @router.post("/{workflow_id}/publish", response_model=WorkflowDefinitionOut, summary="发布流程") async def publish_definition( request: Request, workflow_id: str, db: AsyncSession = Depends(get_db), ): """发布流程""" user_id = request.state.user_id try: workflow = await WorkflowDefinitionService.publish(db, workflow_id, user_id) if not workflow: raise HTTPException(status_code=404, detail="流程定义不存在") await db.commit() return await _build_definition_out(db, workflow) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/{workflow_id}/disable", response_model=WorkflowDefinitionOut, summary="停用流程") async def disable_definition( request: Request, workflow_id: str, db: AsyncSession = Depends(get_db), ): """停用流程""" user_id = request.state.user_id workflow = await WorkflowDefinitionService.disable(db, workflow_id, user_id) if not workflow: raise HTTPException(status_code=404, detail="流程定义不存在") await db.commit() return await _build_definition_out(db, workflow) @router.post("/{workflow_id}/copy", response_model=WorkflowDefinitionOut, summary="复制流程") async def copy_definition( request: Request, workflow_id: str, new_code: str = Query(..., alias="newCode", description="新流程编码"), new_name: str = Query(default=None, alias="newName", description="新流程名称"), db: AsyncSession = Depends(get_db), ): """复制流程""" # 检查新编码唯一性 existing = await WorkflowDefinitionService.get_by_code(db, new_code) if existing: raise HTTPException(status_code=400, detail="新流程编码已存在") user_id = request.state.user_id workflow = await WorkflowDefinitionService.copy(db, workflow_id, new_code, new_name, user_id) if not workflow: raise HTTPException(status_code=404, detail="流程定义不存在") await db.commit() return await _build_definition_out(db, workflow) @router.get("/{workflow_id}/export", summary="导出流程配置") async def export_workflow_config( workflow_id: str, db: AsyncSession = Depends(get_db), ): """导出流程配置为 JSON 文件""" config = await WorkflowDefinitionService.export_config(db, workflow_id) if not config: raise HTTPException(status_code=404, detail="流程不存在") content = json.dumps(config, ensure_ascii=False, indent=2) return StreamingResponse( iter([content]), media_type="application/json", headers={ "Content-Disposition": f'attachment; filename="{config["code"]}.json"' }, ) @router.post("/import/check", response_model=WorkflowImportCheckOut, summary="流程导入预检查") async def check_import_workflow_config( data: WorkflowImportCheckIn, db: AsyncSession = Depends(get_db), ): """导入预检查:检查流程编码是否冲突""" return await WorkflowDefinitionService.check_import(db, data.code) @router.post("/import", response_model=WorkflowDefinitionOut, summary="导入流程配置") async def import_workflow_config( request: Request, data: WorkflowImportIn, db: AsyncSession = Depends(get_db), ): """导入流程配置""" user_id = request.state.user_id try: workflow = await WorkflowDefinitionService.import_config( db, data.model_dump(), user_id ) await db.commit() return await _build_definition_out(db, workflow) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) # ==================== 流程实例 API ==================== @router.post("/instance/start", response_model=WorkflowInstanceOut, summary="发起流程") async def start_instance( request: Request, data: WorkflowInstanceCreate, db: AsyncSession = Depends(get_db), ): """发起流程""" user_id = request.state.user_id try: instance = await WorkflowInstanceService.start( db, workflow_code=data.workflow_code, title=data.title, form_data=data.form_data, user_id=user_id, ) await db.commit() return await _build_instance_out(db, instance) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/instance/list", response_model=PaginatedResponse[WorkflowInstanceListOut], summary="流程实例列表") async def get_instances( page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"), status: str = Query(default=None, description="状态"), title: str = Query(default=None, description="标题"), workflow_name: str = Query(default=None, alias="workflowName", description="流程名称"), instance_no: str = Query(default=None, alias="instanceNo", description="流程编号"), initiator_name: str = Query(default=None, alias="initiatorName", description="发起人姓名"), workflow_id: str = Query(default=None, alias="workflowId", description="流程定义ID"), application_id: str = Query(default=None, alias="applicationId", description="所属应用ID"), db: AsyncSession = Depends(get_db), ): """获取流程实例列表(需 workflow:instance:list 权限)""" items, total = await WorkflowInstanceService.get_list( db, page=page, page_size=page_size, status=status, title=title, workflow_name=workflow_name, instance_no=instance_no, initiator_name=initiator_name, workflow_id=workflow_id, application_id=application_id, ) result_items = [await _build_instance_list_out(db, item) for item in items] return PaginatedResponse(items=result_items, total=total) @router.get("/instance/my-initiated", response_model=PaginatedResponse[WorkflowInstanceListOut], summary="我发起的流程") async def get_my_instances( request: Request, page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"), status: str = Query(default=None, description="状态"), title: str = Query(default=None, description="标题"), workflow_name: str = Query(default=None, alias="workflowName", description="流程名称"), db: AsyncSession = Depends(get_db), ): """获取我发起的流程""" user_id = request.state.user_id items, total = await WorkflowInstanceService.get_list( db, page=page, page_size=page_size, status=status, initiator_id=user_id, title=title, workflow_name=workflow_name, ) result_items = [await _build_instance_list_out(db, item) for item in items] return PaginatedResponse(items=result_items, total=total) @router.get("/instance/{instance_id}", response_model=WorkflowInstanceOut, summary="流程实例详情") async def get_instance( instance_id: str, db: AsyncSession = Depends(get_db), ): """获取流程实例详情""" instance = await WorkflowInstanceService.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/{instance_id}/cancel", response_model=WorkflowInstanceOut, summary="撤回流程") async def cancel_instance( request: Request, instance_id: str, db: AsyncSession = Depends(get_db), ): """撤回流程""" user_id = request.state.user_id try: instance = await WorkflowInstanceService.cancel(db, instance_id, user_id) if not instance: raise HTTPException(status_code=404, detail="流程实例不存在") await db.commit() return await _build_instance_out(db, instance) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/instance/{instance_id}/urge", response_model=MessageResponse, summary="催办") async def urge_instance( request: Request, instance_id: str, db: AsyncSession = Depends(get_db), ): """催办流程""" user_id = request.state.user_id try: await WorkflowInstanceService.urge(db, instance_id, user_id) await db.commit() return MessageResponse(message="催办成功") except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/instance/{instance_id}/logs", response_model=List[WorkflowLogOut], summary="流程日志") async def get_instance_logs( instance_id: str, db: AsyncSession = Depends(get_db), ): """获取流程实例的操作日志""" logs = await WorkflowLogService.get_by_instance(db, instance_id) return [await _build_log_out(db, log) for log in logs] @router.get("/instance/{instance_id}/pending-tasks", response_model=List[WorkflowTaskListOut], summary="流程当前待办") async def get_instance_pending_tasks( instance_id: str, db: AsyncSession = Depends(get_db), ): """获取流程实例当前待处理的任务""" tasks = await WorkflowTaskService.get_instance_pending_tasks(db, instance_id) return [await _build_task_list_out(db, task) for task in tasks] @router.get("/instance/{instance_id}/form-data", summary="获取流程表单数据") async def get_instance_form_data( instance_id: str, db: AsyncSession = Depends(get_db), ): """获取流程实例的表单数据""" instance = await WorkflowInstanceService.get_by_id(db, instance_id) if not instance: raise HTTPException(status_code=404, detail="流程实例不存在") if not instance.form_data_id: return {} try: from online_dev.form_data_manager.service import FormDataService # 创建表单数据服务 service = await FormDataService.create_service(db, instance.form_code) # 获取表单数据 form_data = await service.get(db, instance.form_data_id) return form_data except Exception as e: # 如果获取失败,返回空对象 return {"error": str(e)} @router.get("/instance/{instance_id}/progress", summary="流程执行进度") async def get_instance_progress( instance_id: str, db: AsyncSession = Depends(get_db), ): """ 获取流程实例的执行进度 返回完整的流程节点列表及其状态,包括: - 节点基本信息(ID、名称、类型) - 节点状态(completed/active/pending/skipped/rejected) - 处理人信息(姓名、操作、意见、时间) - 额外操作(加签、转交、委托) - 驳回记录 """ from online_dev.workflow.progress_service import FlowProgressService progress = await FlowProgressService.get_progress(db, instance_id) if not progress: raise HTTPException(status_code=404, detail="流程实例不存在") return progress @router.get("/instance/{instance_id}/documents", summary="获取流程实例关联的单据") async def get_instance_documents( instance_id: str, db: AsyncSession = Depends(get_db), ): """ 获取流程实例关联的已生成单据列表 """ from online_dev.document_generator.service import GeneratedDocumentService # 获取流程实例 instance = await WorkflowInstanceService.get_by_id(db, instance_id) if not instance: raise HTTPException(status_code=404, detail="流程实例不存在") # 获取关联的单据 documents = await GeneratedDocumentService.get_by_instance_id(db, instance_id) return documents @router.post("/instance/{instance_id}/generate-documents", summary="生成流程实例单据") async def generate_instance_documents( request: Request, instance_id: str, db: AsyncSession = Depends(get_db), ): """ 手动生成流程实例绑定的单据 """ from online_dev.document_generator.service import DocumentTemplateService, GeneratedDocumentService from online_dev.document_generator.generator import pdf_generator from online_dev.document_generator.calculation_engine import CalculationEngine from core.file_manager.service import FileManagerService from online_dev.workflow.engine.utils import FormDataUtils from online_dev.workflow.model import WorkflowLog # 获取流程实例 instance = await WorkflowInstanceService.get_by_id(db, instance_id) if not instance: raise HTTPException(status_code=404, detail="流程实例不存在") # 检查流程状态 if instance.status != 'approved': raise HTTPException(status_code=400, detail="只有审批通过的流程才能生成单据") # 获取流程定义 workflow = await WorkflowDefinitionService.get_by_id(db, str(instance.workflow_id)) if not workflow: raise HTTPException(status_code=404, detail="流程定义不存在") # 根据 workflow_code 查找绑定的单据模板 bound_templates = await DocumentTemplateService.get_by_workflow_code(db, workflow.code) if not bound_templates: raise HTTPException(status_code=400, detail="该流程未绑定文档模板") # 加载表单数据 form_data = await FormDataUtils.load_form_data(db, instance.form_code, instance.form_data_id) or {} # 加载流程实例数据 form_data["_instance"] = { "id": str(instance.id), "instance_no": instance.instance_no, "title": instance.title, "status": instance.status, "started_at": str(instance.started_at) if instance.started_at else None, "completed_at": str(instance.completed_at) if instance.completed_at else None, } # 加载审批日志 log_stmt = select(WorkflowLog).where( WorkflowLog.instance_id == instance_id ).order_by(WorkflowLog.sys_create_datetime) log_result = await db.execute(log_stmt) logs = list(log_result.scalars().all()) form_data["_logs"] = [ { "node_name": log.node_name, "action": log.action, "comment": log.comment or "", "created_at": str(log.sys_create_datetime) if log.sys_create_datetime else "", } for log in logs ] # 加载审批任务(包含审批人、审批意见、签名) from online_dev.workflow.model import WorkflowTask from core.user.model import User task_stmt = select(WorkflowTask).where( WorkflowTask.instance_id == instance_id, WorkflowTask.task_type.in_(['approval', 'handle']), WorkflowTask.status.in_(['approved', 'rejected', 'handled']) ).order_by(WorkflowTask.handled_at) task_result = await db.execute(task_stmt) tasks = list(task_result.scalars().all()) # 构建审批任务数据 approval_tasks = [] for task in tasks: # 获取审批人信息 user_stmt = select(User).where(User.id == task.assignee_id) user_result = await db.execute(user_stmt) user = user_result.scalar_one_or_none() task_data = { "node_name": task.node_name, "assignee_name": user.name if user else "", "assignee_username": user.username if user else "", "comment": task.comment or "", "status": task.status, "handled_at": str(task.handled_at) if task.handled_at else "", "signature_url": None, } # 如果有签名,获取签名图片的 base64 或 URL if task.signature_file_id: try: signature_data = await FileManagerService.get_file_as_data_url(db, task.signature_file_id) task_data["signature_url"] = signature_data except Exception as e: logger.warning(f"获取签名图片失败: task_id={task.id}, error={e}") approval_tasks.append(task_data) form_data["_approvals"] = approval_tasks # 按节点名称索引审批数据(方便模板按节点名称访问) approvals_by_node = {} for task_data in approval_tasks: node_name = task_data.get("node_name", "") if node_name: approvals_by_node[node_name] = task_data form_data["_approvals_by_node"] = approvals_by_node # 加载子表数据(用于聚合计算) # 子表数据在 form_data 中以 sub_tables 形式存在,需要展开到顶层 if "sub_tables" in form_data: for sub_table_name, sub_table_data in form_data["sub_tables"].items(): form_data[sub_table_name] = sub_table_data generated_docs = [] user_id = request.state.user_id # 删除该实例已有的单据(重新生成时替换旧单据) existing_docs = await GeneratedDocumentService.get_by_instance_id(db, instance_id) for old_doc in existing_docs: # 删除关联的文件 if old_doc.file_id: await FileManagerService.delete_item(db, old_doc.file_id, hard=True, is_superuser=True) # 删除单据记录 await GeneratedDocumentService.delete(db, old_doc.id, hard=True, auto_commit=False) # 为每个绑定的模板生成文档 for template in bound_templates: try: # 检查模板内容 if not template.template_content: logger.error(f"模板内容为空: {template.code}") raise HTTPException(status_code=400, detail=f"模板内容为空: {template.code}") logger.info(f"生成单据 - 模板: {template.code}, 类型: {template.template_type}, 内容长度: {len(template.template_content)}") # 执行计算规则(如果配置了) template_data = dict(form_data) if template.calculation_rules: try: calculated_values = await CalculationEngine.calculate_all( template.calculation_rules, form_data ) # 将计算结果合并到模板数据中 template_data.update(calculated_values) # 同时添加到 _calculated 命名空间,方便模板引用 template_data["_calculated"] = calculated_values logger.info(f"计算完成 - 模板: {template.code}, 计算结果: {calculated_values}") except Exception as calc_error: logger.warning(f"计算规则执行失败: {template.code}, 错误: {calc_error}") page_config = { "size": template.page_size, "orientation": template.page_orientation, "margin": template.page_margin or {"top": 20, "right": 20, "bottom": 20, "left": 20}, } pdf_bytes = pdf_generator.generate( template_type=template.template_type, template_content=template.template_content, data=template_data, css=template.template_css, page_config=page_config, ) # 验证 PDF 文件头 pdf_header = pdf_bytes[:8] if len(pdf_bytes) >= 8 else pdf_bytes logger.info(f"PDF 生成成功 - 大小: {len(pdf_bytes)} bytes, 文件头: {pdf_header}") if not pdf_bytes.startswith(b'%PDF'): logger.error(f"生成的 PDF 文件头无效: {pdf_header}") raise HTTPException(status_code=500, detail="生成的 PDF 文件无效") page_count = pdf_generator.get_page_count(pdf_bytes) # 保存文件 document_name = f"{template.name}_{instance.instance_no}" filename = f"{document_name}.pdf" file_record = await FileManagerService.upload_file( db=db, file_content=pdf_bytes, filename=filename, file_size=len(pdf_bytes), parent_id=None, is_public=False, source="workflow", ) logger.info(f"文件保存成功 - ID: {file_record.id}, 存储路径: {file_record.storage_path}") # 创建文档记录 doc = await GeneratedDocumentService.create_document( db=db, template=template, file_id=file_record.id, file_size=len(pdf_bytes), page_count=page_count, document_name=document_name, form_data_id=instance.form_data_id, instance_id=instance_id, generator_id=user_id, generate_type="manual", ) generated_docs.append(doc) except Exception as e: raise HTTPException(status_code=500, detail=f"生成单据失败: {str(e)}") await db.commit() return {"message": f"成功生成 {len(generated_docs)} 个单据", "count": len(generated_docs)} # ==================== 任务 API ==================== @router.get("/task/pending", response_model=PaginatedResponse[WorkflowTaskListOut], summary="获取待处理任务") async def get_pending_tasks( request: Request, page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"), task_type: str = Query(default=None, alias="taskType", description="任务类型"), instance_title: str = Query(default=None, alias="instanceTitle", description="流程标题"), workflow_name: str = Query(default=None, alias="workflowName", description="流程名称"), db: AsyncSession = Depends(get_db), ): """获取当前用户的待处理任务""" user_id = request.state.user_id items, total = await WorkflowTaskService.get_pending_tasks( db, user_id=user_id, page=page, page_size=page_size, task_type=task_type, instance_title=instance_title, workflow_name=workflow_name, ) result_items = [await _build_task_list_out(db, item) for item in items] return PaginatedResponse(items=result_items, total=total) @router.get("/task/pending/count", response_model=CountResponse, summary="获取待处理任务数量") async def get_pending_tasks_count( request: Request, db: AsyncSession = Depends(get_db), ): """获取当前用户的待处理任务数量""" user_id = request.state.user_id count = await WorkflowTaskService.get_pending_count(db, user_id) return CountResponse(count=count) @router.get("/task/handled", response_model=PaginatedResponse[WorkflowTaskListOut], summary="获取已处理任务") async def get_handled_tasks( request: Request, page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"), instance_title: str = Query(default=None, alias="instanceTitle", description="流程标题"), workflow_name: str = Query(default=None, alias="workflowName", description="流程名称"), db: AsyncSession = Depends(get_db), ): """获取当前用户的已处理任务""" user_id = request.state.user_id items, total = await WorkflowTaskService.get_handled_tasks( db, user_id=user_id, page=page, page_size=page_size, instance_title=instance_title, workflow_name=workflow_name, ) result_items = [await _build_task_list_out(db, item) for item in items] return PaginatedResponse(items=result_items, total=total) @router.get("/task/cc", response_model=PaginatedResponse[WorkflowTaskListOut], summary="抄送给我") async def get_copy_tasks( request: Request, page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"), instance_title: str = Query(default=None, alias="instanceTitle", description="流程标题"), workflow_name: str = Query(default=None, alias="workflowName", description="流程名称"), is_read: bool = Query(default=None, alias="isRead", description="是否已读,false=仅未读"), db: AsyncSession = Depends(get_db), ): """获取抄送给当前用户的任务""" user_id = request.state.user_id items, total = await WorkflowTaskService.get_copy_tasks( db, user_id=user_id, page=page, page_size=page_size, instance_title=instance_title, workflow_name=workflow_name, is_read=is_read, ) result_items = [await _build_task_list_out(db, item) for item in items] return PaginatedResponse(items=result_items, total=total) @router.get("/task/{task_id}", response_model=WorkflowTaskOut, summary="获取任务详情") async def get_task( task_id: str, db: AsyncSession = Depends(get_db), ): """获取任务详情""" task = await WorkflowTaskService.get_by_id(db, task_id) if not task: raise HTTPException(status_code=404, detail="任务不存在") return await _build_task_out(db, task, include_form=True) @router.post("/task/{task_id}/approve", response_model=WorkflowTaskOut, summary="审批任务") async def approve_task( request: Request, task_id: str, data: TaskApproveInput, db: AsyncSession = Depends(get_db), ): """审批任务(通过/拒绝/驳回)""" user_id = request.state.user_id try: task = await WorkflowTaskService.approve( db, task_id, action=data.action, comment=data.comment, user_id=user_id, return_to=data.return_to, form_data=data.form_data, signature=data.signature, ) await db.commit() return await _build_task_out(db, task) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/task/{task_id}/handle", response_model=WorkflowTaskOut, summary="办理任务") async def handle_task( request: Request, task_id: str, data: TaskHandleInput, db: AsyncSession = Depends(get_db), ): """办理任务""" user_id = request.state.user_id try: task = await WorkflowTaskService.handle(db, task_id, data.comment, user_id, form_data=data.form_data) await db.commit() return await _build_task_out(db, task) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/task/{task_id}/transfer", response_model=WorkflowTaskOut, summary="转交任务") async def transfer_task( request: Request, task_id: str, data: TaskTransferInput, db: AsyncSession = Depends(get_db), ): """转交任务""" user_id = request.state.user_id try: task = await WorkflowTaskService.transfer(db, task_id, data.to_user_id, data.comment, user_id) await db.commit() return await _build_task_out(db, task) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/task/{task_id}/delegate", response_model=WorkflowTaskOut, summary="委派任务") async def delegate_task( request: Request, task_id: str, data: TaskDelegateInput, db: AsyncSession = Depends(get_db), ): """委派任务""" user_id = request.state.user_id try: task = await WorkflowTaskService.delegate(db, task_id, data.to_user_id, data.comment, user_id) await db.commit() return await _build_task_out(db, task) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/task/{task_id}/add-sign", response_model=List[WorkflowTaskOut], summary="加签任务") async def add_sign_task( request: Request, task_id: str, data: TaskAddSignInput, db: AsyncSession = Depends(get_db), ): """加签任务""" user_id = request.state.user_id try: tasks = await WorkflowTaskService.add_sign( db, task_id, sign_type=data.sign_type, to_user_ids=data.to_user_ids, comment=data.comment, user_id=user_id, ) await db.commit() return [await _build_task_out(db, task) for task in tasks] except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/task/{task_id}/read", response_model=WorkflowTaskOut, summary="标记抄送已读") async def mark_task_read( request: Request, task_id: str, db: AsyncSession = Depends(get_db), ): """标记抄送任务为已读""" user_id = request.state.user_id try: task = await WorkflowTaskService.mark_read(db, task_id, user_id) await db.commit() return await _build_task_out(db, task) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/task/{task_id}/revise", response_model=WorkflowTaskOut, summary="修改重提") async def revise_task( request: Request, task_id: str, data: TaskReviseInput, db: AsyncSession = Depends(get_db), ): """修改任务(驳回后重新提交)""" user_id = request.state.user_id try: task = await WorkflowTaskService.revise(db, task_id, data.form_data, data.comment, user_id) await db.commit() return await _build_task_out(db, task) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) # ==================== 管理员 API ==================== @router.post("/admin/check-timeouts", response_model=ResponseModel, summary="手动触发超时检查") async def check_timeouts( request: Request, db: AsyncSession = Depends(get_db), ): """ 手动触发任务超时检查(管理员使用) 正常情况下由定时任务自动执行,此接口用于调试或紧急处理 """ if not getattr(request.state, 'is_superuser', False): raise HTTPException(status_code=403, detail='需要管理员权限') from online_dev.workflow.engine.task_timeout_process import check_and_handle_timeouts await check_and_handle_timeouts(db) await db.commit() return ResponseModel(message='超时检查完成') @router.get("/admin/timeout-tasks", summary="获取即将超时的任务") async def get_timeout_tasks( request: Request, hours: int = Query(default=24, description="查询未来N小时内超时的任务"), db: AsyncSession = Depends(get_db), ): """获取即将超时的任务列表(管理员使用)""" if not getattr(request.state, 'is_superuser', False): raise HTTPException(status_code=403, detail='需要管理员权限') from datetime import datetime, timedelta from online_dev.workflow.model import WorkflowTask, WorkflowInstance from core.user.model import User from sqlalchemy import select now = datetime.now() deadline = now + timedelta(hours=hours) stmt = select(WorkflowTask).where( WorkflowTask.status == 'pending', WorkflowTask.timeout_at.isnot(None), WorkflowTask.timeout_at <= deadline, WorkflowTask.is_deleted == False, ).order_by(WorkflowTask.timeout_at) result = await db.execute(stmt) tasks = result.scalars().all() result_list = [] for task in tasks: # 获取实例 stmt = select(WorkflowInstance).where(WorkflowInstance.id == task.instance_id) inst_result = await db.execute(stmt) instance = inst_result.scalar_one_or_none() # 获取处理人 stmt = select(User).where(User.id == task.assignee_id) user_result = await db.execute(stmt) assignee = user_result.scalar_one_or_none() is_overdue = task.timeout_at <= now if task.timeout_at else False result_list.append({ 'id': str(task.id), 'instance_id': str(task.instance_id), 'instance_title': instance.title if instance else '', 'node_name': task.node_name, 'assignee_id': str(task.assignee_id) if task.assignee_id else '', 'assignee_name': assignee.name if assignee else '', 'timeout_at': task.timeout_at, 'timeout_action': task.timeout_action, 'timeout_notified': task.timeout_notified, 'is_overdue': is_overdue, 'created_at': task.sys_create_datetime, }) return result_list