- test_system_settings: 简化登录方式,直接使用 admin_token fixture + auth_header - test_ticket_executor: DDL操作不再被语法验证拒绝(产品需求),_rejected改为_pass - test_workflow: 修复update endpoint字段名匹配问题
274 lines
8.8 KiB
Python
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)
|