1390 lines
50 KiB
Python
1390 lines
50 KiB
Python
#!/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())
|