Files
ai-agent-admin/backend-fastapi/online_dev/workflow/api.py
T

1499 lines
57 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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