Files
zkxw/backend/app/api/workflow.py
T
2026-09-28 10:49:53 +08:00

521 lines
19 KiB
Python

"""工作流管理接口"""
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, Query
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin
from app.core.response import success_response, error_response
from app.crud import workflow as wf_crud
from app.models.models import SysUser, WfNode, WfProcessDef, WfProcessVersion, WfTask
from app.services.workflow_engine import WorkflowEngine
router = APIRouter(prefix="/web/admin/workflow", tags=["工作流管理"])
# ---------------------------------------------------------------------------
# 业务类型 → 中文名称映射
# ---------------------------------------------------------------------------
_BUSINESS_TYPE_LABELS: Dict[str, str] = {
"leave": "请假申请",
}
def _business_type_label(code: Optional[str]) -> str:
if not code:
return ""
return _BUSINESS_TYPE_LABELS.get(code, code)
# ---------------------------------------------------------------------------
# 业务表单字段提供者注册表
# 新增业务类型时,只需添加一个 provider 函数并注册即可,前端无需改动
# ---------------------------------------------------------------------------
import logging
_form_field_logger = logging.getLogger("workflow.form_fields")
def _leave_form_fields(db: Session, business_id: str) -> List[Dict[str, str]]:
from app.crud import leave as leave_crud
item = leave_crud.get_leave(db, business_id)
if not item:
return []
leave_type_map = {"personal": "事假", "sick": "病假", "annual": "年假", "other": "其他"}
fields = [
{"label": "请假类型", "value": leave_type_map.get(item.get("leaveType", ""), item.get("leaveType", ""))},
{"label": "请假天数", "value": f"{item.get('days', 0)} 天" if item.get("days") else "--"},
{"label": "开始日期", "value": item.get("startDate", "") or "--"},
{"label": "结束日期", "value": item.get("endDate", "") or "--"},
]
if item.get("reason"):
fields.append({"label": "请假原因", "value": item["reason"]})
if item.get("remark"):
fields.append({"label": "备注", "value": item["remark"]})
return fields
_BUSINESS_FORM_FIELD_PROVIDERS: Dict[str, Any] = {
"leave": _leave_form_fields,
}
def _get_business_form_fields(db: Session, business_type: Optional[str], business_id: Optional[str]) -> List[Dict[str, str]]:
if not business_type or not business_id:
return []
provider = _BUSINESS_FORM_FIELD_PROVIDERS.get(business_type)
if not provider:
return []
try:
return provider(db, business_id)
except Exception as e:
_form_field_logger.warning("加载业务表单字段失败: type=%s id=%s err=%s", business_type, business_id, e)
return []
def _enrich_task_list(db: Session, items: List[Dict[str, Any]]) -> None:
starter_ids = {it.get("starterUserId") for it in items if it.get("starterUserId")}
user_name_map: Dict[str, str] = {}
if starter_ids:
rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(starter_ids)).all()
user_name_map = {r.id: r.nick_name for r in rows}
for it in items:
sid = it.get("starterUserId")
it["starterUserName"] = user_name_map.get(sid, sid or "")
it["businessTypeLabel"] = _business_type_label(it.get("businessType"))
# ---------------------------------------------------------------------------
# Pydantic request bodies
# ---------------------------------------------------------------------------
class ProcessDefCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
processCode: str = Field(..., alias="processCode")
processName: str = Field(..., alias="processName")
status: Optional[str] = Field("1", alias="status")
orderNum: Optional[int] = Field(0, alias="orderNum")
remark: Optional[str] = None
class ProcessDefUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
processCode: Optional[str] = Field(None, alias="processCode")
processName: Optional[str] = Field(None, alias="processName")
status: Optional[str] = Field(None, alias="status")
orderNum: Optional[int] = Field(None, alias="orderNum")
remark: Optional[str] = None
class PublishVersionBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
graphJson: Optional[str] = Field(None, alias="graphJson")
nodes: List[Dict[str, Any]] = Field(default_factory=list, alias="nodes")
transitions: List[Dict[str, Any]] = Field(default_factory=list, alias="transitions")
remark: Optional[str] = None
class StartInstanceBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
processCode: str = Field(..., alias="processCode")
businessType: Optional[str] = Field(None, alias="businessType")
businessId: Optional[str] = Field(None, alias="businessId")
variables: Optional[Dict[str, Any]] = None
callbackUrl: Optional[str] = Field(None, alias="callbackUrl")
class TaskActionBody(BaseModel):
"""审批意见:前端传 commentText,兼容 comment。"""
model_config = ConfigDict(populate_by_name=True)
comment: Optional[str] = Field(
default=None,
validation_alias=AliasChoices("comment", "commentText"),
)
# ---------------------------------------------------------------------------
# 流程定义 CRUD
# ---------------------------------------------------------------------------
@router.get("/defs")
async def list_process_defs(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
status: Optional[str] = None,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _user.get("tenant_no", "zkxw")
items, total = wf_crud.list_process_defs(
db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no, user_info=_user
)
tp = wf_crud.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@router.post("/defs")
async def create_process_def(
body: ProcessDefCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = user.get("tenant_no", "zkxw")
obj = wf_crud.create_process_def(
db,
process_code=body.processCode,
process_name=body.processName,
tenant_no=tenant_no,
status=body.status,
order_num=body.orderNum,
remark=body.remark,
user_id=user.get("user_id"),
)
return success_response(wf_crud._row_to_dict(obj), message="创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/defs/{def_id}")
async def update_process_def(
def_id: str,
body: ProcessDefUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
payload: Dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=True)
obj = wf_crud.update_process_def(
db,
def_id,
process_code=payload.get("processCode"),
process_name=payload.get("processName"),
status=payload.get("status"),
order_num=payload.get("orderNum"),
remark=payload.get("remark"),
user_id=user.get("user_id"),
tenant_no=user.get("tenant_no", "zkxw"),
)
return success_response(wf_crud._row_to_dict(obj), message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.delete("/defs/{def_id}")
async def delete_process_def(
def_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
wf_crud.soft_delete_process_def(db, def_id)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 版本发布 / 查看
# ---------------------------------------------------------------------------
@router.post("/defs/{def_id}/publish")
async def publish_version(
def_id: str,
body: PublishVersionBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = user.get("tenant_no", "zkxw")
ver = wf_crud.publish_version(
db,
process_def_id=def_id,
graph_json=body.graphJson or "",
nodes=body.nodes,
transitions=body.transitions,
tenant_no=tenant_no,
remark=body.remark,
user_id=user.get("user_id"),
)
return success_response({"versionId": ver.id, "versionNo": ver.version_no}, message="发布成功")
except ValueError as e:
return error_response(400, str(e))
@router.get("/defs/{def_id}/versions")
async def list_versions(
def_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _user.get("tenant_no", "zkxw")
items = wf_crud.list_versions(db, def_id, tenant_no=tenant_no, user_info=_user)
return success_response(items)
@router.get("/defs/{def_id}/designer")
async def get_designer_data(
def_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
ver = wf_crud.get_latest_version(db, def_id)
if not ver:
return success_response({"graphJson": None, "nodes": [], "transitions": []})
nodes = wf_crud.get_nodes_by_version(db, ver.id)
transitions = wf_crud.get_transitions_by_version(db, ver.id)
return success_response({
"graphJson": ver.graph_json,
"versionId": ver.id,
"versionNo": ver.version_no,
"nodes": [
{"nodeKey": n.node_key, "nodeType": n.node_type, "nodeName": n.node_name, "configJson": n.config_json}
for n in nodes
],
"transitions": [
{"fromNodeKey": t.from_node_key, "toNodeKey": t.to_node_key, "conditionExpr": t.condition_expr}
for t in transitions
],
})
# ---------------------------------------------------------------------------
# 流程实例
# ---------------------------------------------------------------------------
@router.post("/instances")
async def start_instance(
body: StartInstanceBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = user.get("tenant_no", "zkxw")
user_id = user.get("user_id") or user.get("id")
defn = (
db.query(WfProcessDef)
.filter(
WfProcessDef.process_code == body.processCode,
WfProcessDef.del_flag == 0,
)
.first()
)
if not defn:
return error_response(404, "流程定义不存在")
engine = WorkflowEngine(db)
instance = engine.start_process(
process_def_id=defn.id,
business_type=body.businessType,
business_id=body.businessId,
starter_user_id=user_id,
variables=body.variables,
callback_url=body.callbackUrl,
tenant_no=tenant_no,
user_id=user_id,
)
return success_response({
"instanceId": instance.id,
"status": instance.status,
}, message="发起成功")
except ValueError as e:
return error_response(400, str(e))
@router.get("/instances/my")
async def list_my_instances(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
status: Optional[str] = None,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
user_id = user.get("user_id") or user.get("id")
tenant_no = user.get("tenant_no", "zkxw")
items, total = wf_crud.list_my_instances(
db, user_id, page=page, page_size=pageSize,
keyword=keyword, status=status, tenant_no=tenant_no, user_info=user,
)
_enrich_task_list(db, items)
tp = wf_crud.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@router.get("/instances/{instance_id}")
async def get_instance_detail(
instance_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
instance = wf_crud.get_instance(db, instance_id)
if not instance:
return error_response(404, "流程实例不存在")
tasks = db.query(WfTask).filter(
WfTask.instance_id == instance_id,
WfTask.del_flag == 0,
).order_by(WfTask.create_time.asc()).all()
# 查询节点名称映射 node_key → node_name
node_name_map: Dict[str, str] = {}
if instance.process_version_id:
for n in wf_crud.get_nodes_by_version(db, instance.process_version_id):
node_name_map[n.node_key] = n.node_name or n.node_key
# 批量查询用户名
user_ids = {t.assignee_user_id for t in tasks if t.assignee_user_id}
if instance.starter_user_id:
user_ids.add(instance.starter_user_id)
user_name_map: Dict[str, str] = {}
if user_ids:
rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(user_ids)).all()
user_name_map = {r.id: r.nick_name for r in rows}
return success_response({
"id": instance.id,
"businessType": instance.business_type,
"businessTypeLabel": _business_type_label(instance.business_type),
"businessId": instance.business_id,
"businessFormFields": _get_business_form_fields(db, instance.business_type, instance.business_id),
"starterUserId": instance.starter_user_id,
"starterUserName": user_name_map.get(instance.starter_user_id, instance.starter_user_id or ""),
"status": instance.status,
"variablesJson": instance.variables_json,
"callbackUrl": instance.callback_url,
"createTime": instance.create_time.strftime("%Y-%m-%d %H:%M:%S") if instance.create_time else "",
"tasks": [
{
"id": t.id,
"nodeKey": t.node_key,
"nodeName": node_name_map.get(t.node_key, t.node_key or ""),
"assigneeUserId": t.assignee_user_id,
"assigneeUserName": user_name_map.get(t.assignee_user_id, t.assignee_user_id or ""),
"status": t.status,
"commentText": t.comment_text,
"createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
"updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
}
for t in tasks
],
})
# ---------------------------------------------------------------------------
# 待办 / 已办
# ---------------------------------------------------------------------------
@router.get("/tasks/pending")
async def list_pending_tasks(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
user_id = user.get("user_id") or user.get("id")
tenant_no = user.get("tenant_no", "zkxw")
items, total = wf_crud.list_pending_tasks(
db, user_id, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
)
_enrich_task_list(db, items)
tp = wf_crud.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
@router.get("/tasks/completed")
async def list_completed_tasks(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
user_id = user.get("user_id") or user.get("id")
tenant_no = user.get("tenant_no", "zkxw")
items, total = wf_crud.list_completed_tasks(
db, user_id, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
)
_enrich_task_list(db, items)
tp = wf_crud.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
# ---------------------------------------------------------------------------
# 审批 / 驳回
# ---------------------------------------------------------------------------
@router.post("/tasks/{task_id}/approve")
async def approve_task(
task_id: str,
body: TaskActionBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = user.get("tenant_no", "zkxw")
actor_user_id = user.get("user_id") or user.get("id")
engine = WorkflowEngine(db)
instance = engine.approve_task(
task_id, body.comment or "", tenant_no, actor_user_id=actor_user_id
)
return success_response({"instanceId": instance.id, "status": instance.status}, message="审批成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/tasks/{task_id}/reject")
async def reject_task(
task_id: str,
body: TaskActionBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = user.get("tenant_no", "zkxw")
actor_user_id = user.get("user_id") or user.get("id")
engine = WorkflowEngine(db)
instance = engine.reject_task(
task_id, body.comment or "", tenant_no, actor_user_id=actor_user_id
)
return success_response({"instanceId": instance.id, "status": instance.status}, message="已驳回")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 按业务查询所有审批历史(跨实例)
# ---------------------------------------------------------------------------
@router.get("/instances/by-business/{business_type}/{business_id}")
async def get_business_all_tasks(
business_type: str,
business_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tasks = wf_crud.get_all_tasks_for_business(db, business_type, business_id)
business_form_fields = _get_business_form_fields(db, business_type, business_id)
return success_response({
"businessType": business_type,
"businessId": business_id,
"businessFormFields": business_form_fields,
"tasks": tasks,
})