"""多级审批流引擎 — 支持串行、并行、条件审批、超时处理与流程中断恢复。 对标 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"