1499 lines
57 KiB
Python
1499 lines
57 KiB
Python
#!/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
|