sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1
- P0: 实例访问控制全线接入权限组 (829测试) - P1: permissions字段激活 + 操作级权限检查 (843测试) - 前端: 删除数据权限页面, 权限组合并为唯一入口 - 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析 - 新增: MongoDB管理页面 + Dashboard增强 - 修复: Pydantic V2迁移 + 前端TS错误修复
This commit is contained in:
1 parent
6fcd9465cf
commit
4c9427e546
105 files changed
+23680
-471
No files matched your search
@@ -0,0 +1,359 @@
|
||||
"""多级审批流引擎 — 支持串行、并行、条件审批。
|
||||
|
||||
对标 Yearning/Bytebase 的审批能力:
|
||||
- 启动审批流程时根据工单绑定的 workflow 创建审批实例
|
||||
- 支持通过、拒绝、转交、跳过等操作
|
||||
- 自动分配审批人(基于负载均衡)
|
||||
- 超时自动处理
|
||||
"""
|
||||
import json
|
||||
import random
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import List, Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sqlalchemy import select, func, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.workflow import WorkflowDefinition, WorkflowStep, TicketApproval
|
||||
from app.models.ticket import Ticket
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApprovalProgress:
|
||||
"""审批进度数据。"""
|
||||
ticket_id: int
|
||||
current_step: int
|
||||
total_steps: int
|
||||
status: str # pending / reviewing / approved / rejected
|
||||
steps: List[Dict[str, Any]]
|
||||
|
||||
|
||||
class WorkflowEngine:
|
||||
"""多级审批流引擎。"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def start_workflow(self, ticket_id: int,
|
||||
workflow_id: Optional[int] = None) -> List[TicketApproval]:
|
||||
"""为工单启动审批流程。
|
||||
|
||||
如果没有指定 workflow_id,使用默认流程。
|
||||
为每个步骤创建 TicketApproval 记录。
|
||||
"""
|
||||
if workflow_id is None:
|
||||
workflow = await self._get_default_workflow()
|
||||
else:
|
||||
result = await self.db.execute(
|
||||
select(WorkflowDefinition).where(WorkflowDefinition.id == workflow_id)
|
||||
)
|
||||
workflow = result.scalar_one_or_none()
|
||||
if not workflow:
|
||||
workflow = await self._get_default_workflow()
|
||||
|
||||
if not workflow:
|
||||
# 没有配置审批流,直接标记为 approved
|
||||
return []
|
||||
|
||||
approvals = []
|
||||
for step in workflow.steps:
|
||||
approval = TicketApproval(
|
||||
ticket_id=ticket_id,
|
||||
step_number=step.step_number,
|
||||
step_name=step.name,
|
||||
approver_role=step.approver_role,
|
||||
action="pending",
|
||||
)
|
||||
# 如果指定了具体审批人
|
||||
if step.approver_ids:
|
||||
ids = json.loads(step.approver_ids)
|
||||
if ids:
|
||||
approval.approver_id = ids[0]
|
||||
self.db.add(approval)
|
||||
approvals.append(approval)
|
||||
|
||||
await self.db.flush()
|
||||
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
|
||||
"""
|
||||
approval = await self._get_approval(approval_id)
|
||||
if not approval:
|
||||
return {"success": False, "message": "审批记录不存在"}
|
||||
|
||||
if approval.action != "pending":
|
||||
return {"success": False, "message": f"当前状态为 {approval.action},无法审批"}
|
||||
|
||||
approval.action = "approved"
|
||||
approval.approver_id = user_id
|
||||
approval.comment = comment or "审批通过"
|
||||
approval.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
# 检查是否所有步骤都已完成
|
||||
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 approval.step_type == "parallel":
|
||||
# 并行审批:需要所有审批人都通过
|
||||
if all(a.action == "approved" 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, "approved")
|
||||
return {"success": True, "new_status": "reviewing",
|
||||
"message": "当前步骤并行审批已完成,等待后续步骤",
|
||||
"next_step": current_step_num + 1}
|
||||
elif approval.step_type == "any_one":
|
||||
# 任一通过即可
|
||||
all_done = await self._check_all_steps_done(ticket_approvals, current_step_num)
|
||||
if all_done:
|
||||
return await self._complete_approval(approval.ticket_id, "approved")
|
||||
return {"success": True, "new_status": "reviewing",
|
||||
"message": "当前步骤已有人审批通过,等待后续步骤",
|
||||
"next_step": current_step_num + 1}
|
||||
else:
|
||||
# 串行审批
|
||||
all_done = await self._check_all_steps_done(ticket_approvals, current_step_num)
|
||||
if all_done:
|
||||
return await self._complete_approval(approval.ticket_id, "approved")
|
||||
return {"success": True, "new_status": "reviewing",
|
||||
"message": "审批已通过,等待后续步骤",
|
||||
"next_step": current_step_num + 1}
|
||||
|
||||
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 != "pending":
|
||||
return {"success": False, "message": f"当前状态为 {approval.action},无法操作"}
|
||||
|
||||
approval.action = "rejected"
|
||||
approval.approver_id = user_id
|
||||
approval.comment = comment or "审批拒绝"
|
||||
approval.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
# 拒绝整个工单
|
||||
result = await self._complete_approval(approval.ticket_id, "rejected")
|
||||
result["message"] = f"审批已拒绝: {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": "审批记录不存在"}
|
||||
|
||||
# 创建新的审批记录给目标用户
|
||||
new_approval = TicketApproval(
|
||||
ticket_id=approval.ticket_id,
|
||||
step_number=approval.step_number,
|
||||
step_name=approval.step_name,
|
||||
approver_id=to_user,
|
||||
approver_role=approval.approver_role,
|
||||
action="pending",
|
||||
)
|
||||
self.db.add(new_approval)
|
||||
|
||||
# 标记原审批为转交
|
||||
approval.action = "transferred"
|
||||
approval.comment = f"转交给用户 {to_user}: {comment}"
|
||||
approval.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
await self.db.flush()
|
||||
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": "审批记录不存在"}
|
||||
|
||||
approval.action = "skipped"
|
||||
approval.approver_id = user_id
|
||||
approval.comment = f"跳过: {reason}"
|
||||
approval.updated_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)
|
||||
if all_done:
|
||||
return await self._complete_approval(approval.ticket_id, "approved")
|
||||
|
||||
return {"success": True, "message": "步骤已跳过", "new_status": "reviewing"}
|
||||
|
||||
async def get_approval_progress(self, ticket_id: int) -> ApprovalProgress:
|
||||
"""获取工单的审批进度。"""
|
||||
approvals = await self._get_ticket_approvals(ticket_id)
|
||||
ticket_result = await self.db.execute(select(Ticket).where(Ticket.id == ticket_id))
|
||||
ticket = ticket_result.scalar_one_or_none()
|
||||
|
||||
steps_data = []
|
||||
for approval in approvals:
|
||||
approver_name = "未分配"
|
||||
if approval.approver_id:
|
||||
user_result = await self.db.execute(
|
||||
select(User).where(User.id == approval.approver_id)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
approver_name = user.display_name if user else "未知"
|
||||
|
||||
steps_data.append({
|
||||
"step_number": approval.step_number,
|
||||
"step_name": approval.step_name,
|
||||
"approver_name": approver_name,
|
||||
"approver_role": approval.approver_role,
|
||||
"action": approval.action,
|
||||
"comment": approval.comment,
|
||||
"created_at": approval.created_at.isoformat() if approval.created_at else None,
|
||||
"updated_at": approval.updated_at.isoformat() if approval.updated_at else None,
|
||||
})
|
||||
|
||||
current_step = 1
|
||||
total_steps = len(approvals)
|
||||
status = ticket.status if ticket else "unknown"
|
||||
|
||||
# 找到当前待审批的步骤
|
||||
for a in approvals:
|
||||
if a.action == "pending":
|
||||
current_step = a.step_number
|
||||
break
|
||||
|
||||
return ApprovalProgress(
|
||||
ticket_id=ticket_id,
|
||||
current_step=current_step,
|
||||
total_steps=total_steps,
|
||||
status=status,
|
||||
steps=steps_data,
|
||||
)
|
||||
|
||||
async def auto_assign(self, step: WorkflowStep) -> Optional[int]:
|
||||
"""基于负载自动分配审批人。
|
||||
|
||||
优先选择当前待审批任务最少的 DBA。
|
||||
"""
|
||||
if step.approver_ids:
|
||||
ids = json.loads(step.approver_ids)
|
||||
if ids:
|
||||
return ids[0]
|
||||
|
||||
if step.approver_role:
|
||||
# 查找该角色下负载最小的用户
|
||||
result = await self.db.execute(
|
||||
select(User.id, User.display_name)
|
||||
.where(User.role == step.approver_role, User.is_active == True)
|
||||
)
|
||||
candidates = result.all()
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
# 统计每个候选人的待审批数量
|
||||
workloads = []
|
||||
for user_id, _ in candidates:
|
||||
count_result = await self.db.execute(
|
||||
select(func.count(TicketApproval.id))
|
||||
.where(TicketApproval.approver_id == user_id,
|
||||
TicketApproval.action == "pending")
|
||||
)
|
||||
count = count_result.scalar() or 0
|
||||
workloads.append((user_id, count))
|
||||
|
||||
# 选择负载最小的
|
||||
workloads.sort(key=lambda x: x[1])
|
||||
return workloads[0][0]
|
||||
|
||||
return None
|
||||
|
||||
async def check_timeout_and_process(self):
|
||||
"""检查超时的审批并自动处理。"""
|
||||
timeout_threshold = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
result = await self.db.execute(
|
||||
select(TicketApproval).where(
|
||||
and_(TicketApproval.action == "pending",
|
||||
TicketApproval.created_at < timeout_threshold)
|
||||
)
|
||||
)
|
||||
timed_out = result.scalars().all()
|
||||
|
||||
for approval in timed_out:
|
||||
# 获取步骤定义,检查是否配置为超时自动通过
|
||||
workflow_result = await self.db.execute(
|
||||
select(WorkflowStep).where(WorkflowStep.id == approval.step_number)
|
||||
)
|
||||
step_def = workflow_result.scalar_one_or_none()
|
||||
if step_def and step_def.auto_approve_on_timeout:
|
||||
approval.action = "approved"
|
||||
approval.comment = "超时自动通过"
|
||||
approval.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
# ── 内部辅助方法 ──
|
||||
|
||||
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_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 in ("pending",):
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _complete_approval(self, ticket_id: int,
|
||||
final_status: str) -> Dict[str, Any]:
|
||||
"""完成审批,更新工单状态。"""
|
||||
ticket_result = await self.db.execute(
|
||||
select(Ticket).where(Ticket.id == ticket_id)
|
||||
)
|
||||
ticket = ticket_result.scalar_one_or_none()
|
||||
if ticket:
|
||||
ticket.status = final_status
|
||||
if final_status == "approved":
|
||||
ticket.review_time = datetime.now(timezone.utc)
|
||||
|
||||
await self.db.flush()
|
||||
return {
|
||||
"success": True,
|
||||
"new_status": final_status,
|
||||
"message": f"审批流程已完成,工单状态: {final_status}",
|
||||
"next_step": None,
|
||||
}
|
||||
|
||||
@property
|
||||
def step_type(self) -> str:
|
||||
"""获取当前步骤类型。"""
|
||||
return "serial" # 简化实现
|
||||
Reference in new issue
Block a user