Files
opsxsql/backend/app/services/workflow_engine.py
T

1320 lines
51 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.
"""多级审批流引擎 — 支持串行、并行、条件审批、超时处理与流程中断恢复。
对标 Yearning/Bytebase 的审批能力:
- 启动审批流程时根据工单绑定的 workflow 创建审批实例
- 支持通过、拒绝、转交、跳过、中断、恢复等操作
- 动态审批人分配(基于表达式、负载均衡、上级指派)
- 条件判断增强(支持复杂表达式和字段引用)
- 超时自动处理(支持多种超时策略)
- 完整的执行日志记录
"""
import json
import asyncio
import random
import hashlib
import re
from datetime import datetime, timezone, timedelta
from typing import List, Optional, Dict, Any, Callable, Awaitable, Tuple
from dataclasses import dataclass, field
from enum import Enum
from sqlalchemy import select, func, and_, or_, update, desc
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.workflow import (
WorkflowDefinition, WorkflowStep, WorkflowVersion,
TicketApproval, WorkflowExecutionLog
)
from app.models.ticket import Ticket
from app.models.user import User
# ═══════════════════════════════════════════════════════════
# 数据类定义
# ═══════════════════════════════════════════════════════════
class WorkflowStatus(str, Enum):
"""工作流执行状态。"""
PENDING = "pending"
RUNNING = "running"
APPROVED = "approved"
REJECTED = "rejected"
INTERRUPTED = "interrupted" # 被中断
TIMEOUT = "timeout"
SKIPPED = "skipped"
CANCELLED = "cancelled"
class ApprovalAction(str, Enum):
"""审批动作类型。"""
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
TRANSFERRED = "transferred"
SKIPPED = "skipped"
AUTO_APPROVED = "auto_approved"
INTERRUPTED = "interrupted"
class TimeoutStrategy(str, Enum):
"""超时处理策略。"""
AUTO_APPROVE = "auto_approve"
AUTO_REJECT = "auto_reject"
ESCALATE = "escalate" # 升级到上级
NOTIFY = "notify" # 仅通知
@dataclass
class ApprovalProgress:
"""审批进度数据。"""
ticket_id: int
current_step: int
total_steps: int
status: str
steps: List[Dict[str, Any]]
can_interrupt: bool = False
can_resume: bool = False
interrupt_reason: Optional[str] = None
@dataclass
class ConditionResult:
"""条件判断结果。"""
passed: bool
field: str
operator: str
expected: Any
actual: Any
message: str = ""
@dataclass
class ApproverAssignment:
"""审批人分配结果。"""
user_id: Optional[int]
assignment_type: str # role / specific / leader / load_balance / creator / expression
reason: str = ""
# ═══════════════════════════════════════════════════════════
# 流程引擎核心类
# ═══════════════════════════════════════════════════════════
class WorkflowEngine:
"""多级审批流引擎 — 高性能、条件化、可中断。"""
def __init__(self, db: AsyncSession):
self.db = db
# 步骤类型处理器映射
self._step_handlers: Dict[str, Callable[..., Awaitable[Dict[str, Any]]]] = {
"serial": self._handle_serial_step,
"parallel": self._handle_parallel_step,
"any_one": self._handle_any_one_step,
"condition": self._handle_condition_step,
}
# 超时策略处理器
self._timeout_handlers: Dict[str, Callable[..., Awaitable[None]]] = {
TimeoutStrategy.AUTO_APPROVE.value: self._timeout_auto_approve,
TimeoutStrategy.AUTO_REJECT.value: self._timeout_auto_reject,
TimeoutStrategy.ESCALATE.value: self._timeout_escalate,
TimeoutStrategy.NOTIFY.value: self._timeout_notify,
}
# ═══════════════════════════════════════════════════════════
# 公共 API
# ═══════════════════════════════════════════════════════════
async def start_workflow(
self,
ticket_id: int,
workflow_id: Optional[int] = None,
context: Optional[Dict[str, Any]] = None,
) -> List[TicketApproval]:
"""为工单启动审批流程。
Args:
ticket_id: 工单ID
workflow_id: 指定流程ID,None 使用默认流程
context: 流程上下文数据(用于条件判断)
Returns:
创建的审批记录列表
"""
workflow = await self._get_workflow(workflow_id)
if not workflow:
return []
# 获取当前激活版本
current_version = workflow.get_current_version()
version_id = current_version.id if current_version else None
# 检查触发条件
if workflow.trigger_conditions:
should_trigger = await self._evaluate_trigger_conditions(
workflow.trigger_conditions, ticket_id, context
)
if not should_trigger:
# 条件不满足,直接通过(无需审批)
return []
# 创建审批记录
approvals = []
for step in workflow.steps:
# 检查步骤准入条件
if step.entry_conditions:
should_enter = await self._evaluate_conditions(
step.entry_conditions, ticket_id, context
)
if not should_enter:
continue # 跳过不满足条件的步骤
# 动态分配审批人
assignment = await self._resolve_approver(step, ticket_id, context)
approval = TicketApproval(
ticket_id=ticket_id,
workflow_id=workflow.id,
version_id=version_id,
step_number=step.step_number,
step_name=step.name,
step_type=step.step_type,
approver_role=step.approver_role,
approver_id=assignment.user_id,
action=ApprovalAction.PENDING.value,
)
self.db.add(approval)
approvals.append(approval)
await self.db.flush()
# 记录流程启动日志
await self._log_event(
workflow_id=workflow.id,
ticket_id=ticket_id,
event_type="workflow_started",
event_description=f"审批流程已启动,共 {len(approvals)} 个步骤",
context_data={"step_count": len(approvals), "context": context},
)
# 预加载超时处理任务(后台执行)
if workflow.flow_config and workflow.flow_config.get("timeout_hours"):
asyncio.create_task(self._schedule_timeout_check(ticket_id))
return approvals
async def approve(
self,
approval_id: int,
user_id: int,
comment: str = "",
) -> Dict[str, Any]:
"""审批通过。
Returns:
dict with keys: success, new_status, message, next_step, step_completed
"""
approval = await self._get_approval(approval_id)
if not approval:
return {"success": False, "message": "审批记录不存在"}
if approval.action not in (ApprovalAction.PENDING.value,):
return {"success": False, "message": f"当前状态为 {approval.action},无法审批"}
# 检查权限(审批人本人或有代理权限的管理员)
has_right = await self._check_approve_permission(approval, user_id)
if not has_right:
return {"success": False, "message": "您无权审批此工单"}
# 更新审批记录
approval.action = ApprovalAction.APPROVED.value
approval.approver_id = user_id
approval.comment = comment or "审批通过"
approval.completed_at = datetime.now(timezone.utc)
# 获取步骤类型
step_type = approval.step_type or "serial"
handler = self._step_handlers.get(step_type, self._handle_serial_step)
result = await handler(approval, action="approved")
# 记录日志
await self._log_event(
workflow_id=approval.workflow_id,
ticket_id=approval.ticket_id,
approval_id=approval.id,
event_type="approved",
event_description=f"步骤 {approval.step_number} ({approval.step_name}) 审批通过",
actor_id=user_id,
context_data={"comment": comment, "result": result},
)
return result
async def reject(
self,
approval_id: int,
user_id: int,
comment: str = "",
) -> Dict[str, Any]:
"""审批拒绝。任一拒绝则整个工单拒绝。"""
approval = await self._get_approval(approval_id)
if not approval:
return {"success": False, "message": "审批记录不存在"}
if approval.action not in (ApprovalAction.PENDING.value,):
return {"success": False, "message": f"当前状态为 {approval.action},无法操作"}
approval.action = ApprovalAction.REJECTED.value
approval.approver_id = user_id
approval.comment = comment or "审批拒绝"
approval.completed_at = datetime.now(timezone.utc)
# 拒绝整个工单
result = await self._complete_approval(approval.ticket_id, WorkflowStatus.REJECTED)
result["message"] = f"审批已拒绝: {comment}"
# 记录日志
await self._log_event(
workflow_id=approval.workflow_id,
ticket_id=approval.ticket_id,
approval_id=approval.id,
event_type="rejected",
event_description=f"步骤 {approval.step_number} 审批拒绝,流程终止",
actor_id=user_id,
context_data={"comment": comment},
)
return result
async def transfer(
self,
approval_id: int,
from_user: int,
to_user: int,
comment: str = "",
) -> Dict[str, Any]:
"""转交审批给其他人。"""
approval = await self._get_approval(approval_id)
if not approval:
return {"success": False, "message": "审批记录不存在"}
# 验证转交权限
if approval.approver_id != from_user:
from_user_obj = await self.db.get(User, from_user)
if not from_user_obj or from_user_obj.role != "admin":
return {"success": False, "message": "您无权转交此审批"}
# 标记原审批为转交
approval.action = ApprovalAction.TRANSFERRED.value
approval.comment = f"转交给用户 {to_user}: {comment}"
approval.completed_at = datetime.now(timezone.utc)
# 创建新的审批记录给目标用户
new_approval = TicketApproval(
ticket_id=approval.ticket_id,
workflow_id=approval.workflow_id,
version_id=approval.version_id,
step_number=approval.step_number,
step_name=approval.step_name,
step_type=approval.step_type,
approver_id=to_user,
approver_role=approval.approver_role,
transfer_from=from_user,
transfer_reason=comment,
action=ApprovalAction.PENDING.value,
)
self.db.add(new_approval)
await self.db.flush()
# 记录日志
await self._log_event(
workflow_id=approval.workflow_id,
ticket_id=approval.ticket_id,
event_type="transferred",
event_description=f"审批从用户 {from_user} 转交给用户 {to_user}",
actor_id=from_user,
context_data={"from_user": from_user, "to_user": to_user, "reason": comment},
)
return {"success": True, "message": "审批已转交", "new_approval_id": new_approval.id}
async def skip_step(
self,
approval_id: int,
user_id: int,
reason: str = "",
) -> Dict[str, Any]:
"""管理员跳过当前步骤。"""
approval = await self._get_approval(approval_id)
if not approval:
return {"success": False, "message": "审批记录不存在"}
# 验证管理员权限
user = await self.db.get(User, user_id)
if not user or user.role not in ("admin", "dba"):
return {"success": False, "message": "无权跳过步骤"}
approval.action = ApprovalAction.SKIPPED.value
approval.approver_id = user_id
approval.comment = f"跳过: {reason}"
approval.completed_at = datetime.now(timezone.utc)
# 检查是否全部完成
ticket_approvals = await self._get_ticket_approvals(approval.ticket_id)
current_step_num = approval.step_number
all_done = await self._check_all_steps_done(ticket_approvals, current_step_num)
result = {"success": True, "message": "步骤已跳过", "new_status": "reviewing"}
if all_done:
complete_result = await self._complete_approval(approval.ticket_id, WorkflowStatus.APPROVED)
result.update(complete_result)
# 记录日志
await self._log_event(
workflow_id=approval.workflow_id,
ticket_id=approval.ticket_id,
approval_id=approval.id,
event_type="skipped",
event_description=f"步骤 {approval.step_number} 被跳过",
actor_id=user_id,
context_data={"reason": reason},
)
return result
async def interrupt_workflow(
self,
ticket_id: int,
user_id: int,
reason: str = "",
) -> Dict[str, Any]:
"""中断流程(暂停审批,可后续恢复)。
用于需要补充材料或等待外部事件的场景。
"""
ticket = await self._get_ticket(ticket_id)
if not ticket:
return {"success": False, "message": "工单不存在"}
user = await self.db.get(User, user_id)
if not user or user.role not in ("admin", "dba"):
return {"success": False, "message": "无权中断流程"}
# 将所有 pending 的审批标记为 interrupted
approvals = await self._get_ticket_approvals(ticket_id)
interrupted_count = 0
for approval in approvals:
if approval.action == ApprovalAction.PENDING.value:
approval.action = ApprovalAction.INTERRUPTED.value
approval.comment = f"流程中断: {reason}"
interrupted_count += 1
# 更新工单状态
ticket.status = WorkflowStatus.INTERRUPTED.value
# 记录日志
await self._log_event(
workflow_id=approvals[0].workflow_id if approvals else None,
ticket_id=ticket_id,
event_type="workflow_interrupted",
event_description=f"流程被中断: {reason}",
actor_id=user_id,
context_data={"reason": reason, "interrupted_steps": interrupted_count},
)
await self.db.flush()
return {
"success": True,
"message": f"流程已中断,共 {interrupted_count} 个待审批步骤被暂停",
"interrupted_count": interrupted_count,
}
async def resume_workflow(
self,
ticket_id: int,
user_id: int,
reason: str = "",
) -> Dict[str, Any]:
"""恢复被中断的流程。"""
ticket = await self._get_ticket(ticket_id)
if not ticket:
return {"success": False, "message": "工单不存在"}
if ticket.status != WorkflowStatus.INTERRUPTED.value:
return {"success": False, "message": "当前流程状态不允许恢复"}
user = await self.db.get(User, user_id)
if not user or user.role not in ("admin", "dba"):
return {"success": False, "message": "无权恢复流程"}
# 将所有 interrupted 的审批恢复为 pending
approvals = await self._get_ticket_approvals(ticket_id)
resumed_count = 0
for approval in approvals:
if approval.action == ApprovalAction.INTERRUPTED.value:
approval.action = ApprovalAction.PENDING.value
approval.comment = f"流程恢复: {reason}"
resumed_count += 1
# 恢复工单状态
ticket.status = WorkflowStatus.RUNNING.value
# 记录日志
await self._log_event(
workflow_id=approvals[0].workflow_id if approvals else None,
ticket_id=ticket_id,
event_type="workflow_resumed",
event_description=f"流程已恢复: {reason}",
actor_id=user_id,
context_data={"reason": reason, "resumed_steps": resumed_count},
)
await self.db.flush()
return {
"success": True,
"message": f"流程已恢复,共 {resumed_count} 个步骤重新激活",
"resumed_count": resumed_count,
}
async def cancel_workflow(
self,
ticket_id: int,
user_id: int,
reason: str = "",
) -> Dict[str, Any]:
"""取消流程(不可恢复)。"""
ticket = await self._get_ticket(ticket_id)
if not ticket:
return {"success": False, "message": "工单不存在"}
# 取消所有 pending 的审批
approvals = await self._get_ticket_approvals(ticket_id)
for approval in approvals:
if approval.action in (ApprovalAction.PENDING.value, ApprovalAction.INTERRUPTED.value):
approval.action = ApprovalAction.REJECTED.value
approval.comment = f"流程取消: {reason}"
approval.completed_at = datetime.now(timezone.utc)
# 更新工单状态
ticket.status = WorkflowStatus.CANCELLED.value
# 记录日志
await self._log_event(
workflow_id=approvals[0].workflow_id if approvals else None,
ticket_id=ticket_id,
event_type="workflow_cancelled",
event_description=f"流程被取消: {reason}",
actor_id=user_id,
context_data={"reason": reason},
)
await self.db.flush()
return {"success": True, "message": "流程已取消"}
async def get_approval_progress(self, ticket_id: int) -> ApprovalProgress:
"""获取工单的审批进度(优化版本 — 单次查询)。"""
# 使用预加载优化查询
result = await self.db.execute(
select(TicketApproval, User.display_name.label("approver_name"))
.outerjoin(User, TicketApproval.approver_id == User.id)
.where(TicketApproval.ticket_id == ticket_id)
.order_by(TicketApproval.step_number)
)
rows = result.all()
ticket_result = await self.db.execute(
select(Ticket).where(Ticket.id == ticket_id)
)
ticket = ticket_result.scalar_one_or_none()
steps_data = []
current_step = 1
total_steps = len(rows)
status = ticket.status if ticket else "unknown"
for approval, approver_name in rows:
steps_data.append({
"step_number": approval.step_number,
"step_name": approval.step_name,
"step_type": approval.step_type,
"approver_name": approver_name or ("未分配" if not approval.approver_id else "未知"),
"approver_role": approval.approver_role,
"action": approval.action,
"comment": approval.comment,
"is_timeout": approval.is_timeout,
"created_at": approval.created_at.isoformat() if approval.created_at else None,
"updated_at": approval.updated_at.isoformat() if approval.updated_at else None,
"completed_at": approval.completed_at.isoformat() if approval.completed_at else None,
})
if approval.action == ApprovalAction.PENDING.value and current_step <= approval.step_number:
current_step = approval.step_number
# 是否可以中断/恢复
can_interrupt = status in (WorkflowStatus.RUNNING.value, WorkflowStatus.PENDING.value)
can_resume = status == WorkflowStatus.INTERRUPTED.value
return ApprovalProgress(
ticket_id=ticket_id,
current_step=current_step,
total_steps=total_steps,
status=status,
steps=steps_data,
can_interrupt=can_interrupt,
can_resume=can_resume,
)
async def auto_assign(self, step: WorkflowStep) -> Optional[int]:
"""基于负载自动分配审批人。
支持多种分配策略:
- load_balance: 选择当前待审批任务最少的用户
- round_robin: 轮询分配
- random: 随机分配
"""
assignment = step.approver_assignment or {}
strategy = assignment.get("type", "load_balance")
if step.approver_ids:
ids = step.approver_ids if isinstance(step.approver_ids, list) else json.loads(step.approver_ids)
if ids:
if strategy == "round_robin":
# 简单的轮询:选择最近被分配次数最少的
return await self._round_robin_assign(ids)
elif strategy == "random":
return random.choice(ids)
else:
# 负载均衡(默认)
return await self._load_balance_assign(ids)
if step.approver_role:
result = await self.db.execute(
select(User.id)
.where(User.role == step.approver_role, User.is_active == True)
)
candidates = [r[0] for r in result.all()]
if candidates:
if strategy == "random":
return random.choice(candidates)
return await self._load_balance_assign(candidates)
return None
async def check_timeout_and_process(self) -> Dict[str, Any]:
"""检查超时的审批并自动处理。
批量处理,支持多种超时策略。
Returns:
处理结果统计
"""
stats = {"checked": 0, "auto_approved": 0, "auto_rejected": 0, "escalated": 0, "notified": 0}
# 批量获取超时审批
timeout_threshold = datetime.now(timezone.utc) - timedelta(hours=24)
result = await self.db.execute(
select(TicketApproval, WorkflowStep)
.outerjoin(
WorkflowStep,
and_(
WorkflowStep.workflow_id == TicketApproval.workflow_id,
WorkflowStep.step_number == TicketApproval.step_number,
)
)
.where(
and_(
TicketApproval.action == ApprovalAction.PENDING.value,
TicketApproval.is_timeout == False,
TicketApproval.created_at < timeout_threshold,
)
)
)
timed_out_approvals = result.all()
for approval, step_def in timed_out_approvals:
stats["checked"] += 1
approval.is_timeout = True
approval.timeout_handled_at = datetime.now(timezone.utc)
# 确定超时策略
strategy = TimeoutStrategy.NOTIFY.value
if step_def and step_def.auto_approve_on_timeout:
strategy = TimeoutStrategy.AUTO_APPROVE.value
elif approval.workflow_id:
workflow = await self.db.get(WorkflowDefinition, approval.workflow_id)
if workflow and workflow.flow_config:
strategy = workflow.flow_config.get("timeout_strategy", TimeoutStrategy.NOTIFY.value)
# 执行对应的超时处理
handler = self._timeout_handlers.get(strategy, self._timeout_notify)
await handler(approval, step_def)
if strategy == TimeoutStrategy.AUTO_APPROVE.value:
stats["auto_approved"] += 1
elif strategy == TimeoutStrategy.AUTO_REJECT.value:
stats["auto_rejected"] += 1
elif strategy == TimeoutStrategy.ESCALATE.value:
stats["escalated"] += 1
else:
stats["notified"] += 1
await self.db.flush()
return stats
# ═══════════════════════════════════════════════════════════
# 步骤类型处理器
# ═══════════════════════════════════════════════════════════
async def _handle_serial_step(self, approval: TicketApproval, action: str) -> Dict[str, Any]:
"""处理串行审批步骤。"""
ticket_approvals = await self._get_ticket_approvals(approval.ticket_id)
current_step_num = approval.step_number
all_done = await self._check_all_steps_done(ticket_approvals, current_step_num)
if all_done:
return await self._complete_approval(approval.ticket_id, WorkflowStatus.APPROVED)
return {
"success": True,
"new_status": "reviewing",
"message": "审批已通过,等待后续步骤",
"next_step": current_step_num + 1,
"step_completed": True,
}
async def _handle_parallel_step(self, approval: TicketApproval, action: str) -> Dict[str, Any]:
"""处理并行审批步骤(会签 — 需要所有审批人都通过)。"""
ticket_approvals = await self._get_ticket_approvals(approval.ticket_id)
current_step_num = approval.step_number
step_approvals = [a for a in ticket_approvals if a.step_number == current_step_num]
if all(a.action in (ApprovalAction.APPROVED.value, ApprovalAction.SKIPPED.value,
ApprovalAction.AUTO_APPROVED.value) for a in step_approvals):
all_done = await self._check_all_steps_done(ticket_approvals, current_step_num)
if all_done:
return await self._complete_approval(approval.ticket_id, WorkflowStatus.APPROVED)
return {
"success": True,
"new_status": "reviewing",
"message": "当前步骤并行审批已完成,等待后续步骤",
"next_step": current_step_num + 1,
"step_completed": True,
}
pending_count = sum(1 for a in step_approvals if a.action == ApprovalAction.PENDING.value)
return {
"success": True,
"new_status": "reviewing",
"message": f"并行审批中,还需 {pending_count} 人审批",
"next_step": current_step_num,
"step_completed": False,
"pending_count": pending_count,
}
async def _handle_any_one_step(self, approval: TicketApproval, action: str) -> Dict[str, Any]:
"""处理或签审批步骤(任一审批人通过即可)。"""
ticket_approvals = await self._get_ticket_approvals(approval.ticket_id)
current_step_num = approval.step_number
# 将同步骤其他 pending 标记为 skipped
step_approvals = [a for a in ticket_approvals if a.step_number == current_step_num]
for sa in step_approvals:
if sa.id != approval.id and sa.action == ApprovalAction.PENDING.value:
sa.action = ApprovalAction.SKIPPED.value
sa.comment = "或签模式:其他人已通过"
sa.completed_at = datetime.now(timezone.utc)
all_done = await self._check_all_steps_done(ticket_approvals, current_step_num)
if all_done:
return await self._complete_approval(approval.ticket_id, WorkflowStatus.APPROVED)
return {
"success": True,
"new_status": "reviewing",
"message": "当前步骤已有人审批通过,等待后续步骤",
"next_step": current_step_num + 1,
"step_completed": True,
}
async def _handle_condition_step(self, approval: TicketApproval, action: str) -> Dict[str, Any]:
"""处理条件分支步骤。"""
# 获取步骤定义
result = await self.db.execute(
select(WorkflowStep).where(WorkflowStep.id == approval.id)
)
step_def = result.scalar_one_or_none()
if step_def and step_def.transition_conditions:
conditions = step_def.transition_conditions
condition_passed = await self._evaluate_conditions(
conditions, approval.ticket_id
)
if not condition_passed:
on_false = conditions.get("on_false_action", "skip")
if on_false == "reject":
return await self._complete_approval(approval.ticket_id, WorkflowStatus.REJECTED)
elif on_false == "goto_step":
goto_step = conditions.get("goto_step_number")
return {
"success": True,
"new_status": "reviewing",
"message": f"条件不满足,跳转至步骤 {goto_step}",
"next_step": goto_step,
"step_completed": True,
}
return await self._handle_serial_step(approval, action)
# ═══════════════════════════════════════════════════════════
# 条件判断引擎
# ═══════════════════════════════════════════════════════════
async def _evaluate_trigger_conditions(
self,
conditions_config: Dict[str, Any],
ticket_id: int,
context: Optional[Dict[str, Any]] = None,
) -> bool:
"""评估流程触发条件。"""
conditions = conditions_config.get("conditions", [])
logic = conditions_config.get("logic", "AND").upper()
return await self._evaluate_condition_list(conditions, logic, ticket_id, context)
async def _evaluate_conditions(
self,
conditions_config: Dict[str, Any],
ticket_id: int,
context: Optional[Dict[str, Any]] = None,
) -> bool:
"""评估条件列表。"""
conditions = conditions_config.get("conditions", [])
logic = conditions_config.get("logic", "AND").upper()
return await self._evaluate_condition_list(conditions, logic, ticket_id, context)
async def _evaluate_condition_list(
self,
conditions: List[Dict[str, Any]],
logic: str,
ticket_id: int,
context: Optional[Dict[str, Any]] = None,
) -> bool:
"""评估条件列表,支持 AND/OR 逻辑。"""
if not conditions:
return True
results = []
for cond in conditions:
result = await self._evaluate_single_condition(cond, ticket_id, context)
results.append(result.passed)
if logic == "AND":
return all(results)
elif logic == "OR":
return any(results)
return all(results)
async def _evaluate_single_condition(
self,
condition: Dict[str, Any],
ticket_id: int,
context: Optional[Dict[str, Any]] = None,
) -> ConditionResult:
"""评估单个条件,支持丰富的运算符和字段引用。
支持的运算符:
== / != / > / >= / < / <= / in / not_in / contains / starts_with / ends_with
regex / exists / empty
"""
field = condition.get("field", "")
operator = condition.get("operator", "==")
expected = condition.get("value")
# 获取字段实际值
actual = await self._resolve_field_value(field, ticket_id, context)
passed = False
message = ""
try:
if operator == "==":
passed = actual == expected
elif operator == "!=":
passed = actual != expected
elif operator == ">":
passed = actual is not None and expected is not None and actual > expected
elif operator == ">=":
passed = actual is not None and expected is not None and actual >= expected
elif operator == "<":
passed = actual is not None and expected is not None and actual < expected
elif operator == "<=":
passed = actual is not None and expected is not None and actual <= expected
elif operator == "in":
passed = actual in expected if isinstance(expected, (list, tuple, set)) else False
elif operator == "not_in":
passed = actual not in expected if isinstance(expected, (list, tuple, set)) else True
elif operator == "contains":
passed = expected in actual if isinstance(actual, (str, list, dict)) else False
elif operator == "starts_with":
passed = str(actual).startswith(str(expected))
elif operator == "ends_with":
passed = str(actual).endswith(str(expected))
elif operator == "regex":
passed = bool(re.search(str(expected), str(actual)))
elif operator == "exists":
passed = actual is not None
elif operator == "empty":
passed = actual is None or (isinstance(actual, (str, list, dict)) and len(actual) == 0)
else:
message = f"未知运算符: {operator}"
passed = False
except Exception as e:
message = f"条件评估异常: {e}"
passed = False
return ConditionResult(
passed=passed,
field=field,
operator=operator,
expected=expected,
actual=actual,
message=message,
)
async def _resolve_field_value(
self,
field: str,
ticket_id: int,
context: Optional[Dict[str, Any]] = None,
) -> Any:
"""解析字段引用,支持多层嵌套访问。
支持格式:
- ticket.title 工单字段
- ticket.sql_type 工单SQL类型
- ticket.risk_level 工单风险等级
- ticket.creator_id 工单创建者
- database.instance_id 数据源实例ID
- database.name 数据库名称
- context.xxx 上下文数据
- env.xxx 环境变量
"""
parts = field.split(".")
if len(parts) < 2:
# 尝试从上下文直接获取
if context and field in context:
return context[field]
return None
source = parts[0]
attr_path = parts[1:]
if source == "ticket":
ticket = await self._get_ticket(ticket_id)
if not ticket:
return None
value = ticket
for attr in attr_path:
if value is None:
return None
if isinstance(value, dict):
value = value.get(attr)
else:
value = getattr(value, attr, None)
return value
elif source == "context":
if context is None:
return None
value = context
for attr in attr_path:
if value is None:
return None
if isinstance(value, dict):
value = value.get(attr)
else:
return None
return value
elif source == "env":
# 从工单获取环境相关数据
attr_name = ".".join(attr_path)
ticket = await self._get_ticket(ticket_id)
if ticket and hasattr(ticket, attr_name):
return getattr(ticket, attr_name)
return None
return None
# ═══════════════════════════════════════════════════════════
# 审批人分配
# ═══════════════════════════════════════════════════════════
async def _resolve_approver(
self,
step: WorkflowStep,
ticket_id: int,
context: Optional[Dict[str, Any]] = None,
) -> ApproverAssignment:
"""解析审批人分配策略。"""
assignment = step.approver_assignment or {}
strategy = assignment.get("type", "role")
if strategy == "specific" and step.approver_ids:
ids = step.approver_ids if isinstance(step.approver_ids, list) else json.loads(step.approver_ids)
if ids:
return ApproverAssignment(user_id=ids[0], assignment_type="specific")
elif strategy == "role" and step.approver_role:
result = await self.db.execute(
select(User.id).where(User.role == step.approver_role, User.is_active == True)
)
candidates = [r[0] for r in result.all()]
if candidates:
user_id = await self._load_balance_assign(candidates)
return ApproverAssignment(user_id=user_id, assignment_type="role")
elif strategy == "load_balance" and step.approver_ids:
ids = step.approver_ids if isinstance(step.approver_ids, list) else json.loads(step.approver_ids)
if ids:
user_id = await self._load_balance_assign(ids)
return ApproverAssignment(user_id=user_id, assignment_type="load_balance")
elif strategy == "leader":
# 基于工单创建者的上级
ticket = await self._get_ticket(ticket_id)
if ticket and ticket.creator_id:
# 简化实现:查找同角色的 DBA/Admin
result = await self.db.execute(
select(User.id).where(
User.role.in_(["admin", "dba"]),
User.is_active == True,
).limit(1)
)
leader = result.scalar_one_or_none()
if leader:
return ApproverAssignment(user_id=leader, assignment_type="leader")
elif strategy == "creator":
ticket = await self._get_ticket(ticket_id)
if ticket:
return ApproverAssignment(user_id=ticket.creator_id, assignment_type="creator")
elif strategy == "expression":
expression = assignment.get("expression", "")
resolved = await self._resolve_expression(expression, ticket_id, context)
if resolved:
return ApproverAssignment(user_id=resolved, assignment_type="expression")
# fallback
fallback = assignment.get("fallback_user_id")
if fallback:
return ApproverAssignment(user_id=fallback, assignment_type="expression", reason="expression_fallback")
# 默认策略:自动分配
user_id = await self.auto_assign(step)
return ApproverAssignment(user_id=user_id, assignment_type="auto")
async def _resolve_expression(
self,
expression: str,
ticket_id: int,
context: Optional[Dict[str, Any]] = None,
) -> Optional[int]:
"""解析表达式获取审批人ID。
支持: ${ticket.creator.manager_id} 等嵌套引用
"""
if not expression:
return None
# 提取变量引用 ${...}
pattern = r'\$\{([^}]+)\}'
match = re.search(pattern, expression)
if not match:
# 直接返回整数值
try:
return int(expression)
except ValueError:
return None
var_path = match.group(1)
value = await self._resolve_field_value(var_path, ticket_id, context)
return int(value) if value is not None else None
async def _load_balance_assign(self, user_ids: List[int]) -> Optional[int]:
"""基于负载均衡选择审批人(选择待审批最少的用户)。"""
if not user_ids:
return None
if len(user_ids) == 1:
return user_ids[0]
workloads = []
for uid in user_ids:
count_result = await self.db.execute(
select(func.count(TicketApproval.id))
.where(
TicketApproval.approver_id == uid,
TicketApproval.action == ApprovalAction.PENDING.value,
)
)
count = count_result.scalar() or 0
workloads.append((uid, count))
workloads.sort(key=lambda x: x[1])
return workloads[0][0]
async def _round_robin_assign(self, user_ids: List[int]) -> Optional[int]:
"""轮询分配审批人。"""
if not user_ids:
return None
# 选择最近被分配次数最少的
counts = []
for uid in user_ids:
count_result = await self.db.execute(
select(func.count(TicketApproval.id))
.where(
TicketApproval.approver_id == uid,
TicketApproval.created_at > datetime.now(timezone.utc) - timedelta(days=7),
)
)
count = count_result.scalar() or 0
counts.append((uid, count))
counts.sort(key=lambda x: x[1])
return counts[0][0]
# ═══════════════════════════════════════════════════════════
# 超时处理策略
# ═══════════════════════════════════════════════════════════
async def _timeout_auto_approve(self, approval: TicketApproval, step_def: Optional[WorkflowStep] = None):
"""超时自动通过。"""
approval.action = ApprovalAction.AUTO_APPROVED.value
approval.comment = "超时自动通过"
approval.completed_at = datetime.now(timezone.utc)
# 触发步骤完成检查
await self._handle_serial_step(approval, "timeout")
await self._log_event(
workflow_id=approval.workflow_id,
ticket_id=approval.ticket_id,
approval_id=approval.id,
event_type="timeout",
event_description=f"步骤 {approval.step_number} 超时自动通过",
context_data={"strategy": "auto_approve"},
)
async def _timeout_auto_reject(self, approval: TicketApproval, step_def: Optional[WorkflowStep] = None):
"""超时自动拒绝。"""
approval.action = ApprovalAction.REJECTED.value
approval.comment = "超时自动拒绝"
approval.completed_at = datetime.now(timezone.utc)
await self._complete_approval(approval.ticket_id, WorkflowStatus.REJECTED)
await self._log_event(
workflow_id=approval.workflow_id,
ticket_id=approval.ticket_id,
approval_id=approval.id,
event_type="timeout",
event_description=f"步骤 {approval.step_number} 超时自动拒绝",
context_data={"strategy": "auto_reject"},
)
async def _timeout_escalate(self, approval: TicketApproval, step_def: Optional[WorkflowStep] = None):
"""超时升级处理。"""
# 将审批升级给管理员
result = await self.db.execute(
select(User.id).where(User.role == "admin", User.is_active == True).limit(1)
)
admin_id = result.scalar_one_or_none()
if admin_id:
approval.approver_id = admin_id
approval.comment = f"超时升级,转交给管理员"
# 重置超时标记,给管理员新的处理时间
approval.is_timeout = False
approval.created_at = datetime.now(timezone.utc)
await self._log_event(
workflow_id=approval.workflow_id,
ticket_id=approval.ticket_id,
approval_id=approval.id,
event_type="escalated",
event_description=f"步骤 {approval.step_number} 超时升级给管理员 {admin_id}",
context_data={"strategy": "escalate", "escalated_to": admin_id},
)
async def _timeout_notify(self, approval: TicketApproval, step_def: Optional[WorkflowStep] = None):
"""超时仅通知(不自动处理)。"""
await self._log_event(
workflow_id=approval.workflow_id,
ticket_id=approval.ticket_id,
approval_id=approval.id,
event_type="timeout",
event_description=f"步骤 {approval.step_number} 审批超时,等待人工处理",
context_data={"strategy": "notify"},
)
async def _schedule_timeout_check(self, ticket_id: int):
"""调度超时检查(异步任务)。"""
await asyncio.sleep(3600) # 1小时后检查
await self.check_timeout_and_process()
# ═══════════════════════════════════════════════════════════
# 日志记录
# ═══════════════════════════════════════════════════════════
async def _log_event(
self,
event_type: str,
event_description: str = "",
workflow_id: Optional[int] = None,
ticket_id: Optional[int] = None,
step_id: Optional[int] = None,
approval_id: Optional[int] = None,
actor_id: Optional[int] = None,
context_data: Optional[Dict[str, Any]] = None,
) -> None:
"""记录流程执行日志。"""
# 获取执行者名称
actor_name = None
if actor_id:
user = await self.db.get(User, actor_id)
if user:
actor_name = user.display_name or user.username
log = WorkflowExecutionLog(
workflow_id=workflow_id,
ticket_id=ticket_id,
step_id=step_id,
approval_id=approval_id,
event_type=event_type,
event_description=event_description,
actor_id=actor_id,
actor_name=actor_name,
context_data=context_data,
)
self.db.add(log)
await self.db.flush()
# ═══════════════════════════════════════════════════════════
# 内部辅助方法
# ═══════════════════════════════════════════════════════════
async def _get_workflow(self, workflow_id: Optional[int]) -> Optional[WorkflowDefinition]:
"""获取流程定义,优先使用指定ID,否则获取默认流程。"""
if workflow_id:
result = await self.db.execute(
select(WorkflowDefinition)
.options(selectinload(WorkflowDefinition.steps))
.where(WorkflowDefinition.id == workflow_id, WorkflowDefinition.is_active == True)
)
workflow = result.unique().scalar_one_or_none()
if workflow:
return workflow
# 获取默认流程
result = await self.db.execute(
select(WorkflowDefinition)
.options(selectinload(WorkflowDefinition.steps))
.where(WorkflowDefinition.is_default == True, WorkflowDefinition.is_active == True)
)
return result.unique().scalar_one_or_none()
async def _get_default_workflow(self) -> Optional[WorkflowDefinition]:
"""获取默认审批流程。"""
result = await self.db.execute(
select(WorkflowDefinition)
.where(WorkflowDefinition.is_default == True, WorkflowDefinition.is_active == True)
)
return result.scalar_one_or_none()
async def _get_approval(self, approval_id: int) -> Optional[TicketApproval]:
result = await self.db.execute(
select(TicketApproval).where(TicketApproval.id == approval_id)
)
return result.scalar_one_or_none()
async def _get_ticket(self, ticket_id: int) -> Optional[Ticket]:
result = await self.db.execute(
select(Ticket).where(Ticket.id == ticket_id)
)
return result.scalar_one_or_none()
async def _get_ticket_approvals(self, ticket_id: int) -> List[TicketApproval]:
result = await self.db.execute(
select(TicketApproval)
.where(TicketApproval.ticket_id == ticket_id)
.order_by(TicketApproval.step_number)
)
return result.scalars().all()
async def _check_all_steps_done(
self,
approvals: List[TicketApproval],
current_step: int,
) -> bool:
"""检查当前步骤及之前的所有步骤是否都已完成。"""
for a in approvals:
if a.step_number <= current_step and a.action == ApprovalAction.PENDING.value:
return False
return True
async def _check_approve_permission(self, approval: TicketApproval, user_id: int) -> bool:
"""检查用户是否有审批权限。"""
# 审批人本人
if approval.approver_id == user_id:
return True
# 管理员可以代理审批
user = await self.db.get(User, user_id)
if user and user.role == "admin":
return True
# 角色匹配(如果未指定具体审批人)
if approval.approver_role and not approval.approver_id:
if user and user.role == approval.approver_role:
return True
return False
async def _complete_approval(
self,
ticket_id: int,
final_status: WorkflowStatus,
) -> Dict[str, Any]:
"""完成审批,更新工单状态。"""
ticket = await self._get_ticket(ticket_id)
if ticket:
ticket.status = final_status.value
if final_status == WorkflowStatus.APPROVED:
ticket.review_time = datetime.now(timezone.utc)
await self.db.flush()
# 记录完成日志
await self._log_event(
workflow_id=None,
ticket_id=ticket_id,
event_type="workflow_completed" if final_status == WorkflowStatus.APPROVED else f"workflow_{final_status.value}",
event_description=f"审批流程已完成,工单状态: {final_status.value}",
context_data={"final_status": final_status.value},
)
return {
"success": True,
"new_status": final_status.value,
"message": f"审批流程已完成,工单状态: {final_status.value}",
"next_step": None,
"step_completed": True,
}
@property
def step_type(self) -> str:
"""获取当前步骤类型。"""
return "serial"