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