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

1390 lines
50 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 -*-
"""
工作流服务层
数据权限:
- 使用 get_list_with_data_scope() 自动应用数据权限
- 支持本人、本部门、本部门及下级、全部等数据范围
"""
from datetime import datetime
from typing import Optional, List, Tuple
from sqlalchemy import select, update, func, and_
from sqlalchemy.ext.asyncio import AsyncSession
from online_dev.workflow.model import (
WorkflowDefinition,
WorkflowInstance,
WorkflowTask,
WorkflowLog,
)
from app.data_scope_utils import get_data_scope_filter, apply_data_scope_to_conditions
from utils.context import get_current_user_info_from_context
# 资源类型(用于数据权限配置)
RESOURCE_TYPE = "workflow"
RESOURCE_DISPLAY_NAME = "工作流管理"
class WorkflowDefinitionService:
"""流程定义服务"""
@staticmethod
async def get_list(
db: AsyncSession,
page: int = 1,
page_size: int = 20,
application_id: str = None,
all_apps: bool = False,
name: str = None,
code: str = None,
workflow_type: str = None,
category: str = None,
status: str = None,
form_code: str = None,
) -> Tuple[List[WorkflowDefinition], int]:
"""获取流程定义列表"""
stmt = select(WorkflowDefinition).where(WorkflowDefinition.is_deleted == False)
# 应用过滤
if all_apps:
pass # 不过滤,返回所有应用的流程
elif application_id:
stmt = stmt.where(WorkflowDefinition.application_id == application_id)
else:
# 如果没有指定 application_id,只返回主应用的工作流(application_id 为 NULL
stmt = stmt.where(WorkflowDefinition.application_id.is_(None))
if name:
stmt = stmt.where(WorkflowDefinition.name.ilike(f"%{name}%"))
if code:
stmt = stmt.where(WorkflowDefinition.code.ilike(f"%{code}%"))
if workflow_type:
stmt = stmt.where(WorkflowDefinition.workflow_type == workflow_type)
if category:
stmt = stmt.where(WorkflowDefinition.category == category)
if status:
stmt = stmt.where(WorkflowDefinition.status == status)
if form_code:
stmt = stmt.where(WorkflowDefinition.form_code == form_code)
# 计算总数
count_stmt = select(func.count()).select_from(stmt.subquery())
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 分页
stmt = stmt.order_by(WorkflowDefinition.sort, WorkflowDefinition.sys_create_datetime.desc())
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return items, total
@staticmethod
async def get_list_with_data_scope(
db: AsyncSession,
page: int = 1,
page_size: int = 20,
application_id: str = None,
all_apps: bool = False,
name: str = None,
code: str = None,
workflow_type: str = None,
category: str = None,
status: str = None,
form_code: str = None,
) -> Tuple[List[WorkflowDefinition], int]:
"""
获取流程定义列表(带数据权限过滤)
自动从上下文获取当前用户信息,应用数据权限过滤
"""
conditions = [WorkflowDefinition.is_deleted == False]
# 应用过滤
if all_apps:
pass # 不过滤,返回所有应用的流程
elif application_id:
conditions.append(WorkflowDefinition.application_id == application_id)
else:
conditions.append(WorkflowDefinition.application_id.is_(None))
if name:
conditions.append(WorkflowDefinition.name.ilike(f"%{name}%"))
if code:
conditions.append(WorkflowDefinition.code.ilike(f"%{code}%"))
if workflow_type:
conditions.append(WorkflowDefinition.workflow_type == workflow_type)
if category:
conditions.append(WorkflowDefinition.category == category)
if status:
conditions.append(WorkflowDefinition.status == status)
if form_code:
conditions.append(WorkflowDefinition.form_code == form_code)
# 获取数据权限过滤条件并应用
data_scope_filter = await get_data_scope_filter(db, RESOURCE_TYPE)
scope_conditions = apply_data_scope_to_conditions(WorkflowDefinition, data_scope_filter)
conditions.extend(scope_conditions)
# 计算总数
stmt = select(WorkflowDefinition).where(and_(*conditions))
count_stmt = select(func.count()).select_from(stmt.subquery())
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 分页
stmt = stmt.order_by(WorkflowDefinition.sort, WorkflowDefinition.sys_create_datetime.desc())
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return items, total
@staticmethod
async def get_by_id(db: AsyncSession, workflow_id: str) -> Optional[WorkflowDefinition]:
"""根据ID获取流程定义"""
stmt = select(WorkflowDefinition).where(
WorkflowDefinition.id == workflow_id,
WorkflowDefinition.is_deleted == False,
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
async def get_by_code(db: AsyncSession, code: str) -> Optional[WorkflowDefinition]:
"""根据编码获取流程定义"""
stmt = select(WorkflowDefinition).where(
WorkflowDefinition.code == code,
WorkflowDefinition.is_deleted == False,
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
async def create(db: AsyncSession, data: dict, user_id: str) -> WorkflowDefinition:
"""创建流程定义"""
from online_dev.workflow.flow_definition_validator import validate_flow_definition_names
flow_def = data.get('flow_definition')
if flow_def:
validate_flow_definition_names(flow_def)
# TODO: 获取表单名称
form_name = ""
# 从上下文获取用户信息
user_info = get_current_user_info_from_context()
workflow = WorkflowDefinition(
application_id=data.get('application_id'),
name=data['name'],
code=data['code'],
workflow_type=data.get('workflow_type', 'other'),
icon=data.get('icon', ''),
icon_bg_color=data.get('icon_bg_color', ''),
category=data.get('category', ''),
description=data.get('description', ''),
form_code=data['form_code'],
form_name=form_name,
document_template_codes=data.get('document_template_codes', []),
flow_definition=data.get('flow_definition', {}),
sort=data.get('sort', 0),
)
# 自动填充创建人和部门
workflow.sys_creator_id = user_id or (user_info.get('user_id') if user_info else None)
if user_info and user_info.get('dept_id'):
workflow.sys_dept_id = user_info.get('dept_id')
db.add(workflow)
await db.flush()
await db.refresh(workflow)
return workflow
@staticmethod
async def update(db: AsyncSession, workflow_id: str, data: dict, user_id: str) -> Optional[WorkflowDefinition]:
"""更新流程定义"""
workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id)
if not workflow:
return None
if 'name' in data and data['name']:
workflow.name = data['name']
if 'workflow_type' in data and data['workflow_type']:
workflow.workflow_type = data['workflow_type']
if 'icon' in data:
workflow.icon = data['icon'] or ''
if 'icon_bg_color' in data:
workflow.icon_bg_color = data['icon_bg_color'] or ''
if 'category' in data:
workflow.category = data['category'] or ''
if 'description' in data:
workflow.description = data['description'] or ''
if 'form_code' in data and data['form_code']:
workflow.form_code = data['form_code']
# TODO: 更新表单名称
if 'document_template_codes' in data:
workflow.document_template_codes = data['document_template_codes'] or []
if 'flow_definition' in data and data['flow_definition']:
from online_dev.workflow.flow_definition_validator import validate_flow_definition_names
validate_flow_definition_names(data['flow_definition'])
workflow.flow_definition = data['flow_definition']
if 'sort' in data:
workflow.sort = data['sort'] if data['sort'] is not None else 0
db.add(workflow)
await db.flush()
await db.refresh(workflow)
return workflow
@staticmethod
async def delete(db: AsyncSession, workflow_id: str) -> bool:
"""删除流程定义(软删除)"""
workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id)
if not workflow:
return False
workflow.is_deleted = True
db.add(workflow)
await db.flush()
return True
@staticmethod
async def batch_delete(db: AsyncSession, ids: List[str]) -> int:
"""批量删除"""
stmt = update(WorkflowDefinition).where(
WorkflowDefinition.id.in_(ids),
WorkflowDefinition.is_deleted == False,
).values(is_deleted=True)
result = await db.execute(stmt)
return result.rowcount
@staticmethod
async def publish(db: AsyncSession, workflow_id: str, user_id: str) -> Optional[WorkflowDefinition]:
"""发布流程"""
from online_dev.workflow.flow_definition_validator import (
validate_flow_definition_for_publish,
validate_flow_definition_names,
validate_flow_definition_subflows_for_publish,
)
workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id)
if not workflow:
return None
flow_def = workflow.flow_definition
if not flow_def:
raise ValueError('流程定义为空,无法发布')
validate_flow_definition_names(flow_def)
validate_flow_definition_for_publish(flow_def)
await validate_flow_definition_subflows_for_publish(db, flow_def)
workflow.status = 'published'
workflow.version += 1
db.add(workflow)
await db.flush()
await db.refresh(workflow)
return workflow
@staticmethod
async def disable(db: AsyncSession, workflow_id: str, user_id: str) -> Optional[WorkflowDefinition]:
"""停用流程"""
workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id)
if not workflow:
return None
workflow.status = 'disabled'
db.add(workflow)
await db.flush()
await db.refresh(workflow)
return workflow
@staticmethod
async def copy(db: AsyncSession, workflow_id: str, new_code: str, new_name: str, user_id: str) -> Optional[WorkflowDefinition]:
"""复制流程"""
workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id)
if not workflow:
return None
new_workflow = WorkflowDefinition(
application_id=workflow.application_id,
name=new_name or f"{workflow.name}_副本",
code=new_code,
category=workflow.category,
description=workflow.description,
form_code=workflow.form_code,
form_name=workflow.form_name,
flow_definition=workflow.flow_definition,
status='draft',
)
db.add(new_workflow)
await db.flush()
await db.refresh(new_workflow)
return new_workflow
@staticmethod
def _definition_to_export_dict(workflow: WorkflowDefinition) -> dict:
"""将流程定义转为可导出的 JSON 结构"""
return {
"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 "",
"form_code": workflow.form_code,
"document_template_codes": workflow.document_template_codes or [],
"flow_definition": workflow.flow_definition or {},
"sort": workflow.sort or 0,
}
@classmethod
async def export_config(cls, db: AsyncSession, workflow_id: str) -> Optional[dict]:
"""导出流程配置"""
workflow = await cls.get_by_id(db, workflow_id)
if not workflow:
return None
return cls._definition_to_export_dict(workflow)
@classmethod
async def check_import(cls, db: AsyncSession, code: str) -> dict:
"""导入预检查:编码是否冲突"""
existing = await cls.get_by_code(db, code) if code else None
return {
"code_exists": existing is not None,
"can_import": existing is None,
}
@classmethod
async def _resolve_form_name(cls, db: AsyncSession, form_code: str) -> str:
"""根据表单编码解析表单名称"""
if not form_code:
return ""
from online_dev.form_manager.model import FormMeta
stmt = select(FormMeta.name).where(
FormMeta.code == form_code,
FormMeta.is_deleted == False,
)
result = await db.execute(stmt)
return result.scalar_one_or_none() or ""
@classmethod
async def import_config(
cls,
db: AsyncSession,
data: dict,
user_id: str,
) -> WorkflowDefinition:
"""导入流程配置(创建新草稿流程)"""
if not data.get("name") or not data.get("code"):
raise ValueError("缺少必要字段: name 或 code")
if not data.get("form_code"):
raise ValueError("缺少必要字段: form_code")
existing = await cls.get_by_code(db, data["code"])
if existing:
raise ValueError(f"流程编码已存在: {data['code']}")
data = dict(data)
data["form_name"] = await cls._resolve_form_name(db, data["form_code"])
return await cls.create(db, data, user_id)
@staticmethod
async def get_categories(db: AsyncSession) -> List[str]:
"""获取所有分类"""
stmt = select(WorkflowDefinition.category).where(
WorkflowDefinition.is_deleted == False,
WorkflowDefinition.category != '',
).distinct()
result = await db.execute(stmt)
return [row[0] for row in result.all()]
class WorkflowInstanceService:
"""流程实例服务"""
@staticmethod
async def start(
db: AsyncSession,
workflow_code: str,
title: str,
form_data: dict,
user_id: str,
) -> WorkflowInstance:
"""发起流程"""
from online_dev.workflow.engine import WorkflowEngine
# 获取流程定义
workflow = await WorkflowDefinitionService.get_by_code(db, workflow_code)
if not workflow:
raise ValueError(f"流程 {workflow_code} 不存在")
if workflow.status != 'published':
raise ValueError(f"流程 {workflow_code} 未发布")
# 使用流程引擎启动流程
engine = WorkflowEngine()
instance = await engine.start(
db=db,
workflow=workflow,
title=title,
form_data=form_data,
initiator_id=user_id,
)
return instance
@staticmethod
async def get_list(
db: AsyncSession,
page: int = 1,
page_size: int = 20,
status: str = None,
initiator_id: str = None,
workflow_id: str = None,
title: str = None,
workflow_name: str = None,
instance_no: str = None,
initiator_name: str = None,
application_id: str = None,
) -> Tuple[List[WorkflowInstance], int]:
"""获取流程实例列表"""
from core.user.model import User
joined_workflow = False
joined_user = False
stmt = select(WorkflowInstance).where(WorkflowInstance.is_deleted == False)
if status:
stmt = stmt.where(WorkflowInstance.status == status)
if initiator_id:
stmt = stmt.where(WorkflowInstance.initiator_id == initiator_id)
if workflow_id:
stmt = stmt.where(WorkflowInstance.workflow_id == workflow_id)
if title:
stmt = stmt.where(WorkflowInstance.title.ilike(f"%{title}%"))
if instance_no:
stmt = stmt.where(WorkflowInstance.instance_no.ilike(f"%{instance_no}%"))
if workflow_name or application_id:
stmt = stmt.join(WorkflowDefinition, WorkflowInstance.workflow_id == WorkflowDefinition.id)
joined_workflow = True
if workflow_name:
stmt = stmt.where(WorkflowDefinition.name.ilike(f"%{workflow_name}%"))
if application_id:
stmt = stmt.where(WorkflowDefinition.application_id == application_id)
if initiator_name:
if not joined_user:
stmt = stmt.join(User, WorkflowInstance.initiator_id == User.id)
joined_user = True
stmt = stmt.where(User.name.ilike(f"%{initiator_name}%"))
# 计算总数
count_stmt = select(func.count()).select_from(stmt.subquery())
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 分页
stmt = stmt.order_by(WorkflowInstance.started_at.desc())
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return items, total
@staticmethod
async def get_by_id(db: AsyncSession, instance_id: str) -> Optional[WorkflowInstance]:
"""根据ID获取流程实例"""
stmt = select(WorkflowInstance).where(
WorkflowInstance.id == instance_id,
WorkflowInstance.is_deleted == False,
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
async def cancel(db: AsyncSession, instance_id: str, user_id: str) -> Optional[WorkflowInstance]:
"""撤回流程"""
instance = await WorkflowInstanceService.get_by_id(db, instance_id)
if not instance:
return None
if instance.initiator_id != user_id:
raise ValueError("只能撤回自己发起的流程")
if instance.status != 'pending':
raise ValueError("只能撤回审批中的流程")
# 如果实例处于延时等待状态,清理延时定时任务
if instance.delay_node_id:
try:
from online_dev.workflow.engine.delay_callback import remove_delay_job
await remove_delay_job(str(instance.id), instance.delay_node_id)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"清理延时任务失败: {e}")
instance.delay_node_id = ""
instance.delay_until = None
instance.status = 'canceled'
instance.completed_at = datetime.now()
db.add(instance)
# 先收集待取消任务的ID,用于清理钉钉待办
pending_stmt = select(WorkflowTask.id).where(
WorkflowTask.instance_id == str(instance.id),
WorkflowTask.status == 'pending',
)
pending_result = await db.execute(pending_stmt)
canceled_task_ids = [str(row[0]) for row in pending_result.all()]
# 取消所有待处理任务
stmt = update(WorkflowTask).where(
WorkflowTask.instance_id == str(instance.id),
WorkflowTask.status == 'pending',
).values(status='canceled')
await db.execute(stmt)
# 清理被取消任务的钉钉待办
for tid in canceled_task_ids:
try:
from core.message.service import NotifyService
await NotifyService.delete_dingtalk_todo(db, "workflow_task", tid)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"撤回-清理钉钉待办失败 task={tid}: {e}")
# 记录日志
log = WorkflowLog(
instance_id=str(instance.id),
action='cancel',
operator_id=user_id,
comment='撤回流程',
)
db.add(log)
await db.flush()
await db.refresh(instance)
return instance
@staticmethod
async def urge(db: AsyncSession, instance_id: str, user_id: str) -> None:
"""催办流程"""
instance = await WorkflowInstanceService.get_by_id(db, instance_id)
if not instance:
raise ValueError("流程实例不存在")
if instance.initiator_id != user_id:
raise ValueError("只能催办自己发起的流程")
if instance.status != 'pending':
raise ValueError("只能催办审批中的流程")
# 获取当前待处理的任务(审批 + 办理)
stmt = select(WorkflowTask).where(
WorkflowTask.instance_id == str(instance.id),
WorkflowTask.status == 'pending',
WorkflowTask.task_type.in_(('approval', 'handle')),
WorkflowTask.is_deleted == False,
)
result = await db.execute(stmt)
pending_tasks = result.scalars().all()
if not pending_tasks:
raise ValueError('当前没有待处理的任务')
from online_dev.workflow.model import WorkflowDefinition
from online_dev.workflow.engine.handlers.notification_service import (
WorkflowNotificationService,
)
stmt = select(WorkflowDefinition).where(
WorkflowDefinition.id == instance.workflow_id,
WorkflowDefinition.is_deleted == False,
)
wf_result = await db.execute(stmt)
workflow = wf_result.scalar_one_or_none()
flow_definition = workflow.flow_definition if workflow else {}
sent_count = await WorkflowNotificationService.send_urge_notifications(
db=db,
instance=instance,
pending_tasks=pending_tasks,
flow_definition=flow_definition,
operator_id=user_id,
)
if sent_count == 0:
raise ValueError('当前节点未开启任务通知,无法发送催办')
# 记录催办日志
log = WorkflowLog(
instance_id=str(instance.id),
node_id=instance.current_node_id,
node_name=instance.current_node_name,
action='urge',
operator_id=user_id,
comment=f'已向 {sent_count} 位处理人发送催办通知',
extra_data={'notified_count': sent_count},
)
db.add(log)
await db.flush()
class WorkflowTaskService:
"""任务服务"""
@staticmethod
async def get_pending_tasks(
db: AsyncSession,
user_id: str,
page: int = 1,
page_size: int = 20,
task_type: str = None,
instance_title: str = None,
workflow_name: str = None,
) -> Tuple[List[WorkflowTask], int]:
"""获取待处理任务"""
stmt = select(WorkflowTask).where(
WorkflowTask.assignee_id == user_id,
WorkflowTask.status == 'pending',
WorkflowTask.is_deleted == False,
)
if task_type:
stmt = stmt.where(WorkflowTask.task_type == task_type)
if instance_title:
stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id)
stmt = stmt.where(WorkflowInstance.title.ilike(f"%{instance_title}%"))
if workflow_name:
if 'WorkflowInstance' not in str(stmt):
stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id)
stmt = stmt.join(WorkflowDefinition, WorkflowInstance.workflow_id == WorkflowDefinition.id)
stmt = stmt.where(WorkflowDefinition.name.ilike(f"%{workflow_name}%"))
# 计算总数
count_stmt = select(func.count()).select_from(stmt.subquery())
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 分页
stmt = stmt.order_by(WorkflowTask.sys_create_datetime.desc())
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return items, total
@staticmethod
async def get_handled_tasks(
db: AsyncSession,
user_id: str,
page: int = 1,
page_size: int = 20,
instance_title: str = None,
workflow_name: str = None,
) -> Tuple[List[WorkflowTask], int]:
"""获取已处理任务"""
# 只查询用户实际处理过的任务(排除 canceled/waiting/read/pending
handled_statuses = ['approved', 'rejected', 'returned', 'transferred', 'delegated', 'handled']
stmt = select(WorkflowTask).where(
WorkflowTask.assignee_id == user_id,
WorkflowTask.is_deleted == False,
WorkflowTask.status.in_(handled_statuses),
)
if instance_title:
stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id)
stmt = stmt.where(WorkflowInstance.title.ilike(f"%{instance_title}%"))
if workflow_name:
if 'WorkflowInstance' not in str(stmt):
stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id)
stmt = stmt.join(WorkflowDefinition, WorkflowInstance.workflow_id == WorkflowDefinition.id)
stmt = stmt.where(WorkflowDefinition.name.ilike(f"%{workflow_name}%"))
# 计算总数
count_stmt = select(func.count()).select_from(stmt.subquery())
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 分页
stmt = stmt.order_by(WorkflowTask.handled_at.desc())
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return items, total
@staticmethod
async def get_instance_pending_tasks(db: AsyncSession, instance_id: str) -> List[WorkflowTask]:
"""获取流程实例当前待处理的任务"""
instance = await WorkflowInstanceService.get_by_id(db, instance_id)
if not instance:
return []
if instance.status in ['approved', 'rejected', 'canceled']:
return []
stmt = select(WorkflowTask).where(
WorkflowTask.instance_id == instance_id,
WorkflowTask.status.in_(['pending', 'waiting']),
WorkflowTask.is_deleted == False,
).order_by(WorkflowTask.sys_create_datetime)
result = await db.execute(stmt)
return list(result.scalars().all())
@staticmethod
async def get_copy_tasks(
db: AsyncSession,
user_id: str,
page: int = 1,
page_size: int = 20,
instance_title: str = None,
workflow_name: str = None,
is_read: bool = None,
) -> Tuple[List[WorkflowTask], int]:
"""获取抄送给我的任务"""
stmt = select(WorkflowTask).where(
WorkflowTask.assignee_id == user_id,
WorkflowTask.task_type == 'copy',
WorkflowTask.is_deleted == False,
)
if is_read is False:
stmt = stmt.where(WorkflowTask.status == 'pending')
elif is_read is True:
stmt = stmt.where(WorkflowTask.status == 'read')
if instance_title:
stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id)
stmt = stmt.where(WorkflowInstance.title.ilike(f"%{instance_title}%"))
if workflow_name:
if 'WorkflowInstance' not in str(stmt):
stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id)
stmt = stmt.join(WorkflowDefinition, WorkflowInstance.workflow_id == WorkflowDefinition.id)
stmt = stmt.where(WorkflowDefinition.name.ilike(f"%{workflow_name}%"))
# 计算总数
count_stmt = select(func.count()).select_from(stmt.subquery())
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# 分页
stmt = stmt.order_by(WorkflowTask.sys_create_datetime.desc())
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
result = await db.execute(stmt)
items = list(result.scalars().all())
return items, total
@staticmethod
async def get_by_id(db: AsyncSession, task_id: str) -> Optional[WorkflowTask]:
"""根据ID获取任务"""
stmt = select(WorkflowTask).where(
WorkflowTask.id == task_id,
WorkflowTask.is_deleted == False,
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
@staticmethod
async def _upload_signature(db: AsyncSession, signature_base64: str, task_id: str, user_id: str) -> Optional[str]:
"""
上传签名图片到文件系统
Args:
db: 数据库会话
signature_base64: base64 格式的签名数据
task_id: 任务ID
user_id: 用户ID
Returns:
文件ID,如果上传失败返回 None
"""
import base64
import logging
from io import BytesIO
from datetime import datetime
logger = logging.getLogger(__name__)
try:
# 解析 base64 数据
if ',' in signature_base64:
# 格式: data:image/png;base64,xxxxx
signature_base64 = signature_base64.split(',')[1]
signature_data = base64.b64decode(signature_base64)
# 生成文件名
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
filename = f"signature_{task_id}_{timestamp}.png"
# 上传到文件系统
from core.file_manager.service import FileManagerService
file_record = await FileManagerService.upload_file(
db=db,
file=BytesIO(signature_data),
filename=filename,
content_type="image/png",
user_id=user_id,
source="workflow", # 自动归类到工作流附件文件夹
is_public=False, # 私有文件
)
logger.info(f"签名上传成功: task_id={task_id}, file_id={file_record.id}")
return str(file_record.id)
except Exception as e:
logger.error(f"签名上传失败: task_id={task_id}, error={e}")
return None
@staticmethod
async def approve(
db: AsyncSession,
task_id: str,
action: str,
comment: str,
user_id: str,
return_to: str = None,
form_data: dict = None,
signature: str = None,
) -> WorkflowTask:
"""审批任务"""
from online_dev.workflow.engine import WorkflowEngine
from online_dev.workflow.engine.base import TaskAction
task = await WorkflowTaskService.get_by_id(db, task_id)
if not task:
raise ValueError("任务不存在")
if task.assignee_id != user_id:
raise ValueError("无权处理此任务")
if task.status != 'pending':
raise ValueError("任务已处理")
# 处理签名
# signature 可能是文件ID(前端已上传)或 base64 数据(需要后端上传)
signature_file_id = None
if signature and action == 'approve':
# 判断是文件ID还是base64数据
# 文件ID通常是 nanoid 格式(21位字母数字加下划线/横线),base64数据通常以 data: 开头或很长
if signature.startswith('data:') or len(signature) > 100:
# base64 数据,需要上传
signature_file_id = await WorkflowTaskService._upload_signature(db, signature, task_id, user_id)
else:
# 已经是文件ID
signature_file_id = signature
task.signature_file_id = signature_file_id
# 如果审批人修改了表单数据,先更新
if form_data:
instance = await WorkflowInstanceService.get_by_id(db, task.instance_id)
if instance and instance.form_code and instance.form_data_id:
try:
from online_dev.form_data_manager.service import FormDataService
service = await FormDataService.create_service(db, instance.form_code)
await service.update(db, instance.form_data_id, form_data)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"审批时更新表单数据失败: {e}")
# 映射操作类型
action_map = {
'approve': TaskAction.APPROVE,
'reject': TaskAction.REJECT,
'return': TaskAction.RETURN,
}
task_action = action_map.get(action)
if not task_action:
raise ValueError(f"无效的操作类型: {action}")
# 使用流程引擎处理任务
engine = WorkflowEngine()
return await engine.complete_task(
db=db,
task=task,
action=task_action,
comment=comment,
user_id=user_id,
return_to=return_to,
)
@staticmethod
async def handle(db: AsyncSession, task_id: str, comment: str, user_id: str, form_data: dict = None) -> WorkflowTask:
"""办理任务"""
from online_dev.workflow.engine import WorkflowEngine
from online_dev.workflow.engine.base import ExecutionContext
task = await WorkflowTaskService.get_by_id(db, task_id)
if not task:
raise ValueError("任务不存在")
if task.assignee_id != user_id:
raise ValueError("无权处理此任务")
if task.status != 'pending':
raise ValueError("任务已处理")
if task.task_type != 'handle':
raise ValueError("此任务不是办理任务")
# 如果办理人修改了表单数据,先更新
if form_data:
instance = await WorkflowInstanceService.get_by_id(db, task.instance_id)
if instance and instance.form_code and instance.form_data_id:
try:
from online_dev.form_data_manager.service import FormDataService
service = await FormDataService.create_service(db, instance.form_code)
await service.update(db, instance.form_data_id, form_data)
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"办理时更新表单数据失败: {e}")
# 更新任务状态
task.status = 'handled'
task.comment = comment
task.handled_at = datetime.now()
db.add(task)
# 完成该任务对应的钉钉待办
try:
from core.message.service import NotifyService
await NotifyService.complete_dingtalk_todo(db, "workflow_task", str(task.id))
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"办理-清理钉钉待办失败 task={task.id}: {e}")
# 记录日志
log = WorkflowLog(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
action='handle',
operator_id=user_id,
comment=comment,
)
db.add(log)
await db.flush()
# 获取实例和流程定义
instance = await WorkflowInstanceService.get_by_id(db, task.instance_id)
workflow = await WorkflowDefinitionService.get_by_id(db, instance.workflow_id)
# 使用流程引擎处理办理完成后的推进
engine = WorkflowEngine()
context = ExecutionContext(
instance=instance,
form_data=form_data or {},
current_user_id=user_id,
flow_definition=workflow.flow_definition,
db=db,
)
await engine._handle_handle_completion(context, task)
return task
@staticmethod
async def transfer(db: AsyncSession, task_id: str, to_user_id: str, comment: str, user_id: str) -> WorkflowTask:
"""转交任务"""
from online_dev.workflow.engine import WorkflowEngine
from core.user.model import User
task = await WorkflowTaskService.get_by_id(db, task_id)
if not task:
raise ValueError("任务不存在")
if task.assignee_id != user_id:
raise ValueError("无权转交此任务")
if task.status != 'pending':
raise ValueError("任务已处理")
# 获取目标用户
stmt = select(User).where(User.id == to_user_id)
result = await db.execute(stmt)
to_user = result.scalar_one_or_none()
if not to_user:
raise ValueError("目标用户不存在")
# 使用流程引擎转交任务
engine = WorkflowEngine()
return await engine.transfer_task(
db=db,
task=task,
to_user=to_user,
comment=comment,
user_id=user_id,
)
@staticmethod
async def get_pending_count(db: AsyncSession, user_id: str) -> int:
"""获取待处理任务数量"""
stmt = select(func.count()).where(
WorkflowTask.assignee_id == user_id,
WorkflowTask.status == 'pending',
WorkflowTask.is_deleted == False,
)
result = await db.execute(stmt)
return result.scalar() or 0
@staticmethod
async def delegate(db: AsyncSession, task_id: str, to_user_id: str, comment: str, user_id: str) -> WorkflowTask:
"""委派任务"""
from core.user.model import User
task = await WorkflowTaskService.get_by_id(db, task_id)
if not task:
raise ValueError("任务不存在")
if task.assignee_id != user_id:
raise ValueError("无权委派此任务")
if task.status != 'pending':
raise ValueError("任务已处理")
if task.task_type not in ('approval', 'handle'):
raise ValueError("此类型任务不支持委派")
# 获取被委派人
stmt = select(User).where(User.id == to_user_id)
result = await db.execute(stmt)
to_user = result.scalar_one_or_none()
if not to_user:
raise ValueError("被委派人不存在")
if str(to_user.id) == user_id:
raise ValueError("不能委派给自己")
# 更新原任务状态为已委派
task.status = 'delegated'
task.comment = comment or f'委派给 {to_user.name or to_user.username}'
task.handled_at = datetime.now()
db.add(task)
await db.flush()
# 完成原任务对应的钉钉待办
try:
from core.message.service import NotifyService
await NotifyService.complete_dingtalk_todo(db, 'workflow_task', str(task.id))
except Exception as e:
logger.warning(f'委派-清理钉钉待办失败 task={task.id}: {e}')
# 创建新任务给被委派人
new_task = WorkflowTask(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
task_type=task.task_type,
status='pending',
assignee_id=str(to_user.id),
parent_task_id=str(task.id),
sign_type='delegate',
timeout_at=task.timeout_at,
timeout_action=task.timeout_action or '',
timeout_notified=False,
)
db.add(new_task)
await db.flush()
try:
from online_dev.workflow.engine.handlers.notification_service import (
WorkflowNotificationService,
)
instance, flow_definition = await WorkflowNotificationService.load_instance_and_flow(
db, new_task
)
if instance:
await WorkflowNotificationService.notify_pending_task(
db=db,
task=new_task,
instance=instance,
flow_definition=flow_definition,
operator_id=user_id,
)
except Exception as notify_err:
import logging
logging.getLogger('online_dev.workflow.service').warning(
f'委派后发送任务通知失败 task={new_task.id}: {notify_err}'
)
# 记录日志
log = WorkflowLog(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
action='delegate',
operator_id=user_id,
comment=f'委派给 {to_user.name or to_user.username}' + (f'{comment}' if comment else ''),
extra_data={
'to_user_id': str(to_user.id),
'to_user_name': to_user.name or to_user.username,
'original_task_id': str(task.id),
},
)
db.add(log)
await db.flush()
await db.refresh(new_task)
return new_task
@staticmethod
async def mark_read(db: AsyncSession, task_id: str, user_id: str) -> WorkflowTask:
"""标记抄送任务为已读"""
task = await WorkflowTaskService.get_by_id(db, task_id)
if not task:
raise ValueError("任务不存在")
if task.assignee_id != user_id:
raise ValueError("无权操作此任务")
if task.task_type != 'copy':
raise ValueError("只能标记抄送任务为已读")
if task.status != 'pending':
raise ValueError("任务已处理")
task.status = 'read'
task.handled_at = datetime.now()
db.add(task)
await db.flush()
await db.refresh(task)
return task
@staticmethod
async def add_sign(
db: AsyncSession,
task_id: str,
sign_type: str,
to_user_ids: list,
comment: str,
user_id: str,
) -> List[WorkflowTask]:
"""加签任务"""
from core.user.model import User
if sign_type not in ('before', 'after', 'parallel'):
raise ValueError("无效的加签类型")
if not to_user_ids:
raise ValueError("请选择加签人")
task = await WorkflowTaskService.get_by_id(db, task_id)
if not task:
raise ValueError("任务不存在")
if task.assignee_id != user_id:
raise ValueError("无权操作此任务")
if task.status != 'pending':
raise ValueError("任务已处理")
if task.task_type not in ('approval', 'handle'):
raise ValueError("此类型任务不支持加签")
# 获取加签人
stmt = select(User).where(User.id.in_(to_user_ids))
result = await db.execute(stmt)
to_users = list(result.scalars().all())
if len(to_users) != len(to_user_ids):
raise ValueError("部分加签人不存在")
# 检查是否包含自己
if user_id in to_user_ids:
raise ValueError("不能加签给自己")
created_tasks = []
to_user_names = [u.name or u.username for u in to_users]
if sign_type == 'before':
# 前加签:当前任务暂停
task.status = 'waiting'
task.comment = f'前加签给 {", ".join(to_user_names)}'
db.add(task)
for to_user in to_users:
new_task = WorkflowTask(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
task_type='approval',
status='pending',
assignee_id=str(to_user.id),
parent_task_id=str(task.id),
sign_type='before',
)
db.add(new_task)
created_tasks.append(new_task)
elif sign_type == 'after':
# 后加签:当前处理人先审批
task.status = 'approved'
task.comment = f'审批通过,后加签给 {", ".join(to_user_names)}'
task.handled_at = datetime.now()
db.add(task)
for to_user in to_users:
new_task = WorkflowTask(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
task_type='approval',
status='pending',
assignee_id=str(to_user.id),
parent_task_id=str(task.id),
sign_type='after',
)
db.add(new_task)
created_tasks.append(new_task)
else: # parallel
# 并行加签
for to_user in to_users:
new_task = WorkflowTask(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
task_type='approval',
status='pending',
assignee_id=str(to_user.id),
parent_task_id=str(task.id),
sign_type='parallel',
)
db.add(new_task)
created_tasks.append(new_task)
# 记录日志
sign_type_labels = {
'before': '前加签',
'after': '后加签',
'parallel': '并行加签',
}
log = WorkflowLog(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
action='add_sign',
operator_id=user_id,
comment=f'{sign_type_labels[sign_type]}{", ".join(to_user_names)}' + (f'{comment}' if comment else ''),
extra_data={
'sign_type': sign_type,
'to_user_ids': to_user_ids,
'to_user_names': to_user_names,
'original_task_id': str(task.id),
},
)
db.add(log)
await db.flush()
for t in created_tasks:
await db.refresh(t)
return created_tasks
@staticmethod
async def revise(db: AsyncSession, task_id: str, form_data: dict, comment: str, user_id: str) -> WorkflowTask:
"""修改任务(驳回后发起人重新提交)"""
from online_dev.workflow.engine import WorkflowEngine
task = await WorkflowTaskService.get_by_id(db, task_id)
if not task:
raise ValueError("任务不存在")
if task.assignee_id != user_id:
raise ValueError("无权操作此任务")
if task.task_type != 'revise':
raise ValueError("此任务不是修改任务")
if task.status != 'pending':
raise ValueError("任务已处理")
instance = await WorkflowInstanceService.get_by_id(db, task.instance_id)
if not instance:
raise ValueError("流程实例不存在")
# 完成当前任务
task.status = 'approved'
task.comment = comment or '已修改并重新提交'
task.handled_at = datetime.now()
db.add(task)
# 记录日志
log = WorkflowLog(
instance_id=str(instance.id),
node_id=task.node_id,
node_name='修改重提',
action='revise',
operator_id=user_id,
comment=comment or '已修改并重新提交',
)
db.add(log)
await db.flush()
# 更新表单数据(前端传入格式: {main: {...}, sub_tables: {...}}
# 注意: FormDataService.update 内部会 db.commit(),所以放在所有 ORM 操作之后
import logging
_logger = logging.getLogger(__name__)
_logger.info(f"revise: form_code={instance.form_code}, form_data_id={instance.form_data_id}")
_logger.info(f"revise: form_data keys={list(form_data.keys()) if form_data else 'None'}")
if form_data and form_data.get('main'):
_logger.info(f"revise: main data keys={list(form_data['main'].keys())}")
if form_data and instance.form_code and instance.form_data_id:
try:
from online_dev.form_data_manager.service import FormDataService
service = await FormDataService.create_service(db, instance.form_code)
_logger.info(f"revise: calling service.update with pk={instance.form_data_id}")
await service.update(db, instance.form_data_id, form_data)
_logger.info(f"revise: form data update success")
except Exception as e:
_logger.error(f"更新表单数据失败: {e}", exc_info=True)
# 重新刷新 instance(因为 FormDataService.update 内部 commit 可能导致 ORM 对象 detached
instance = await WorkflowInstanceService.get_by_id(db, task.instance_id)
# 重新启动流程
engine = WorkflowEngine()
await engine.restart_instance(db, instance, user_id)
task = await WorkflowTaskService.get_by_id(db, task_id)
return task
class WorkflowLogService:
"""日志服务"""
@staticmethod
async def get_by_instance(db: AsyncSession, instance_id: str) -> List[WorkflowLog]:
"""获取流程实例的日志"""
stmt = select(WorkflowLog).where(
WorkflowLog.instance_id == instance_id,
WorkflowLog.is_deleted == False,
).order_by(WorkflowLog.sys_create_datetime)
result = await db.execute(stmt)
return list(result.scalars().all())