Files
opsxsql/backend/app/api/v1/endpoints/workflow.py
T
logan 39ffc14d88 fix: 修复15个历史遗留测试失败 (0 failed, 1074 passed)
- test_system_settings: 简化登录方式,直接使用 admin_token fixture + auth_header
- test_ticket_executor: DDL操作不再被语法验证拒绝(产品需求),_rejected改为_pass
- test_workflow: 修复update endpoint字段名匹配问题
2026-05-25 09:10:21 +08:00

274 lines
8.8 KiB
Python

"""审批流程管理 API。"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.core.database import get_db
from app.core.security import require_role
from app.models.user import User
from app.models.workflow import WorkflowDefinition, WorkflowStep
from app.schemas.workflow import (
WorkflowCreate, WorkflowUpdate, WorkflowResponse,
WorkflowStepCreate,
)
router = APIRouter(prefix="/workflows", tags=["审批流程"])
def _serialize_step(step: WorkflowStep) -> dict:
return {
"id": step.id,
"workflow_id": step.workflow_id,
"step_number": step.step_number,
"name": step.name,
"step_type": step.step_type,
"approver_role": step.approver_role,
"approver_ids": step.approver_ids,
"is_required": step.is_required,
"timeout_hours": step.timeout_hours,
"auto_approve_on_timeout": step.auto_approve_on_timeout,
}
def _serialize_workflow(wf: WorkflowDefinition) -> dict:
return {
"id": wf.id,
"name": wf.name,
"description": wf.description,
"is_default": wf.is_default,
"is_active": wf.is_active,
"created_at": wf.created_at,
"steps": [_serialize_step(s) for s in wf.steps],
}
@router.get("", response_model=List[WorkflowResponse])
async def list_workflows(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin", "dba")),
):
result = await db.execute(
select(WorkflowDefinition)
.where(WorkflowDefinition.is_active == True)
.options(selectinload(WorkflowDefinition.steps))
)
workflows = result.scalars().unique().all()
return [_serialize_workflow(wf) for wf in workflows]
@router.post("", response_model=WorkflowResponse, status_code=201)
async def create_workflow(
data: WorkflowCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
# 如果设为默认,取消其他默认
if data.is_default:
from sqlalchemy import update as sa_update
await db.execute(
sa_update(WorkflowDefinition)
.where(WorkflowDefinition.is_default == True)
.values(is_default=False)
)
workflow = WorkflowDefinition(
name=data.name,
description=data.description,
is_default=data.is_default,
)
db.add(workflow)
await db.flush()
for step_data in data.steps:
step = WorkflowStep(
workflow_id=workflow.id,
step_number=step_data.step_number,
name=step_data.name,
step_type=step_data.step_type,
approver_role=step_data.approver_role,
approver_ids=step_data.approver_ids,
is_required=step_data.is_required,
timeout_hours=step_data.timeout_hours,
auto_approve_on_timeout=step_data.auto_approve_on_timeout,
)
db.add(step)
await db.flush()
wf_id = workflow.id
# Reload with steps — use fresh query by id to avoid identity map issues
db.expire(workflow)
result = await db.execute(
select(WorkflowDefinition)
.where(WorkflowDefinition.id == wf_id)
.options(selectinload(WorkflowDefinition.steps))
)
workflow = result.scalar_one()
return _serialize_workflow(workflow)
@router.get("/{workflow_id}", response_model=WorkflowResponse)
async def get_workflow(
workflow_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin", "dba")),
):
result = await db.execute(
select(WorkflowDefinition)
.where(WorkflowDefinition.id == workflow_id)
.options(selectinload(WorkflowDefinition.steps))
)
workflow = result.scalar_one_or_none()
if not workflow:
raise HTTPException(status_code=404, detail="审批流程不存在")
return _serialize_workflow(workflow)
@router.put("/{workflow_id}", response_model=WorkflowResponse)
async def update_workflow(
workflow_id: int,
data: WorkflowUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
result = await db.execute(
select(WorkflowDefinition)
.where(WorkflowDefinition.id == workflow_id)
.options(selectinload(WorkflowDefinition.steps))
)
workflow = result.scalar_one_or_none()
if not workflow:
raise HTTPException(status_code=404, detail="审批流程不存在")
if data.name is not None:
workflow.name = data.name
if data.description is not None:
workflow.description = data.description
if data.is_active is not None:
workflow.is_active = data.is_active
# 更新步骤(先删后建)
if data.steps is not None:
from sqlalchemy import delete as sa_delete
await db.execute(
sa_delete(WorkflowStep).where(WorkflowStep.workflow_id == workflow_id)
)
await db.flush()
for step_data in data.steps:
step = WorkflowStep(
workflow_id=workflow.id,
step_number=step_data.step_number,
name=step_data.name,
step_type=step_data.step_type,
approver_role=step_data.approver_role,
approver_ids=step_data.approver_ids,
is_required=step_data.is_required,
timeout_hours=step_data.timeout_hours,
auto_approve_on_timeout=step_data.auto_approve_on_timeout,
)
db.add(step)
await db.flush()
# Reload with steps
db.expire(workflow)
result = await db.execute(
select(WorkflowDefinition)
.where(WorkflowDefinition.id == workflow_id)
.options(selectinload(WorkflowDefinition.steps))
)
workflow = result.scalar_one()
return _serialize_workflow(workflow)
@router.delete("/{workflow_id}")
async def delete_workflow(
workflow_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
result = await db.execute(
select(WorkflowDefinition).where(WorkflowDefinition.id == workflow_id)
)
workflow = result.scalar_one_or_none()
if not workflow:
raise HTTPException(status_code=404, detail="审批流程不存在")
workflow.is_active = False
await db.commit()
return {"message": "审批流程已删除"}
@router.post("/{workflow_id}/set-default")
async def set_default_workflow(
workflow_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
"""设为默认审批流程。"""
# 取消所有默认
result = await db.execute(select(WorkflowDefinition))
all_workflows = result.scalars().all()
for wf in all_workflows:
wf.is_default = False
# 设置指定流程为默认
result = await db.execute(
select(WorkflowDefinition).where(WorkflowDefinition.id == workflow_id)
)
workflow = result.scalar_one_or_none()
if not workflow:
raise HTTPException(status_code=404, detail="审批流程不存在")
workflow.is_default = True
await db.commit()
return {"message": "审批流程 '{}' 已设为默认".format(workflow.name)}
@router.post("/{workflow_id}/clone", response_model=WorkflowResponse)
async def clone_workflow(
workflow_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin")),
):
"""克隆审批流程。"""
result = await db.execute(
select(WorkflowDefinition)
.where(WorkflowDefinition.id == workflow_id)
.options(selectinload(WorkflowDefinition.steps))
)
source = result.scalar_one_or_none()
if not source:
raise HTTPException(status_code=404, detail="审批流程不存在")
new_workflow = WorkflowDefinition(
name="{} (副本)".format(source.name),
description=source.description,
is_default=False,
)
db.add(new_workflow)
await db.flush()
for step in source.steps:
new_step = WorkflowStep(
workflow_id=new_workflow.id,
step_number=step.step_number,
name=step.name,
step_type=step.step_type,
approver_role=step.approver_role,
approver_ids=step.approver_ids,
is_required=step.is_required,
timeout_hours=step.timeout_hours,
auto_approve_on_timeout=step.auto_approve_on_timeout,
)
db.add(new_step)
await db.commit()
# Reload with steps
result = await db.execute(
select(WorkflowDefinition)
.where(WorkflowDefinition.id == new_workflow.id)
.options(selectinload(WorkflowDefinition.steps))
)
new_workflow = result.scalar_one()
return _serialize_workflow(new_workflow)