Files
shanxia2.0/backend/app/crud/workflow.py
T
2026-05-20 18:06:21 +08:00

780 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""工作流 CRUD(wf_process_def / wf_process_version / wf_node / wf_transition / wf_process_instance / wf_task)"""
from __future__ import annotations
import json
import math
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.models import (
SysUser,
WfNode,
WfProcessDef,
WfProcessInstance,
WfProcessVersion,
WfTask,
WfTransition,
)
# ---------------------------------------------------------------------------
# 流程定义 wf_process_def
# ---------------------------------------------------------------------------
def _row_to_dict(t: WfProcessDef) -> Dict[str, Any]:
return {
"id": t.id,
"processCode": t.process_code,
"processName": t.process_name,
"status": t.status,
"tenantNo": t.tenant_no,
"orderNum": t.order_num if t.order_num is not None else 0,
"remark": t.remark or "",
"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 "",
}
def list_process_defs(
db: Session,
*,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
status: Optional[str] = None,
tenant_no: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
def _filtered():
q0 = db.query(WfProcessDef).filter(WfProcessDef.del_flag == 0)
if tenant_no:
q0 = q0.filter(WfProcessDef.tenant_no == tenant_no)
if status is not None:
q0 = q0.filter(WfProcessDef.status == status)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(or_(WfProcessDef.process_name.like(like), WfProcessDef.process_code.like(like)))
if user_info:
from app.core.data_scope import apply_data_scope
q0 = apply_data_scope(q0, WfProcessDef, user_info, db, tenant_no)
return q0
total = _filtered().order_by(None).with_entities(func.count(WfProcessDef.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(WfProcessDef.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
result = []
for r in rows:
d = _row_to_dict(r)
ver = (
db.query(WfProcessVersion)
.filter(
and_(
WfProcessVersion.process_def_id == r.id,
WfProcessVersion.is_latest == "1",
WfProcessVersion.del_flag == 0,
)
)
.order_by(WfProcessVersion.version_no.desc())
.first()
)
d["latestVersion"] = ver.version_no if ver else None
result.append(d)
return result, int(total)
def get_process_def(db: Session, def_id: str) -> Optional[WfProcessDef]:
return (
db.query(WfProcessDef)
.filter(and_(WfProcessDef.id == def_id, WfProcessDef.del_flag == 0))
.first()
)
def process_code_exists(
db: Session, code: str, tenant_no: str, exclude_id: Optional[str] = None
) -> bool:
q = db.query(WfProcessDef).filter(
and_(WfProcessDef.process_code == code.strip(), WfProcessDef.tenant_no == tenant_no, WfProcessDef.del_flag == 0)
)
if exclude_id:
q = q.filter(WfProcessDef.id != exclude_id)
return q.first() is not None
def create_process_def(
db: Session,
*,
process_code: str,
process_name: str,
tenant_no: str,
status: Optional[str] = None,
order_num: Optional[int] = None,
remark: Optional[str] = None,
user_id: Optional[str] = None,
) -> WfProcessDef:
code = (process_code or "").strip()
if not code:
raise ValueError("流程编码不能为空")
if process_code_exists(db, code, tenant_no):
raise ValueError("流程编码已存在")
obj = WfProcessDef(
id=generate_uuid(),
process_code=code,
process_name=(process_name or "").strip(),
status=status or "1",
tenant_no=tenant_no,
order_num=order_num if order_num is not None else 0,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def update_process_def(
db: Session,
def_id: str,
*,
process_code: Optional[str] = None,
process_name: Optional[str] = None,
status: Optional[str] = None,
order_num: Optional[int] = None,
remark: Optional[str] = None,
user_id: Optional[str] = None,
tenant_no: Optional[str] = None,
) -> WfProcessDef:
obj = get_process_def(db, def_id)
if not obj:
raise ValueError("流程定义不存在")
if process_code is not None:
new_code = process_code.strip()
if not new_code:
raise ValueError("流程编码不能为空")
tn = tenant_no or obj.tenant_no
if new_code != obj.process_code and process_code_exists(db, new_code, tn, exclude_id=def_id):
raise ValueError("流程编码已存在")
obj.process_code = new_code
if process_name is not None:
obj.process_name = (process_name or "").strip()
if status is not None:
obj.status = status
if order_num is not None:
obj.order_num = order_num
if remark is not None:
obj.remark = (remark or "").strip() or None
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def soft_delete_process_def(db: Session, def_id: str) -> None:
obj = get_process_def(db, def_id)
if not obj:
raise ValueError("流程定义不存在")
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
db.commit()
# ---------------------------------------------------------------------------
# 流程版本 wf_process_version
# ---------------------------------------------------------------------------
def publish_version(
db: Session,
process_def_id: str,
graph_json: str,
nodes: List[Dict[str, Any]],
transitions: List[Dict[str, Any]],
tenant_no: str,
remark: Optional[str] = None,
user_id: Optional[str] = None,
) -> WfProcessVersion:
defn = get_process_def(db, process_def_id)
if not defn:
raise ValueError("流程定义不存在")
db.query(WfProcessVersion).filter(
and_(
WfProcessVersion.process_def_id == process_def_id,
WfProcessVersion.is_latest == "1",
WfProcessVersion.del_flag == 0,
)
).update({"is_latest": "0", "update_time": datetime.now()}, synchronize_session=False)
max_ver = (
db.query(func.max(WfProcessVersion.version_no))
.filter(and_(WfProcessVersion.process_def_id == process_def_id, WfProcessVersion.del_flag == 0))
.scalar()
) or 0
new_ver_no = max_ver + 1
ver = WfProcessVersion(
id=generate_uuid(),
process_def_id=process_def_id,
version_no=new_ver_no,
graph_json=graph_json,
is_latest="1",
tenant_no=tenant_no,
order_num=0,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(ver)
db.flush()
for idx, n in enumerate(nodes):
node = WfNode(
id=generate_uuid(),
process_version_id=ver.id,
node_key=n.get("nodeKey", ""),
node_type=n.get("nodeType", ""),
node_name=n.get("nodeName"),
config_json=n.get("configJson"),
tenant_no=tenant_no,
order_num=idx,
remark=None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(node)
for idx, t in enumerate(transitions):
trans = WfTransition(
id=generate_uuid(),
process_version_id=ver.id,
from_node_key=t.get("fromNodeKey", ""),
to_node_key=t.get("toNodeKey", ""),
condition_expr=t.get("conditionExpr"),
tenant_no=tenant_no,
order_num=idx,
remark=None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(trans)
db.commit()
db.refresh(ver)
return ver
def get_latest_version(db: Session, process_def_id: str) -> Optional[WfProcessVersion]:
return (
db.query(WfProcessVersion)
.filter(
and_(
WfProcessVersion.process_def_id == process_def_id,
WfProcessVersion.is_latest == "1",
WfProcessVersion.del_flag == 0,
)
)
.first()
)
def _version_to_dict(v: WfProcessVersion) -> Dict[str, Any]:
return {
"id": v.id,
"versionNo": v.version_no,
"isLatest": v.is_latest,
"remark": v.remark or "",
"createTime": v.create_time.strftime("%Y-%m-%d %H:%M:%S") if v.create_time else "",
"updateTime": v.update_time.strftime("%Y-%m-%d %H:%M:%S") if v.update_time else "",
}
def list_versions(
db: Session, process_def_id: str, tenant_no: Optional[str] = None, user_info=None
) -> List[Dict[str, Any]]:
q = db.query(WfProcessVersion).filter(
and_(WfProcessVersion.process_def_id == process_def_id, WfProcessVersion.del_flag == 0)
)
if tenant_no:
q = q.filter(WfProcessVersion.tenant_no == tenant_no)
if user_info:
from app.core.data_scope import apply_data_scope
q = apply_data_scope(q, WfProcessVersion, user_info, db, tenant_no)
rows = q.order_by(WfProcessVersion.version_no.desc()).all()
return [_version_to_dict(v) for v in rows]
def get_nodes_by_version(db: Session, version_id: str) -> List[WfNode]:
return (
db.query(WfNode)
.filter(and_(WfNode.process_version_id == version_id, WfNode.del_flag == 0))
.order_by(WfNode.order_num.asc())
.all()
)
def get_transitions_by_version(db: Session, version_id: str) -> List[WfTransition]:
return (
db.query(WfTransition)
.filter(and_(WfTransition.process_version_id == version_id, WfTransition.del_flag == 0))
.order_by(WfTransition.order_num.asc())
.all()
)
# ---------------------------------------------------------------------------
# 流程实例 wf_process_instance
# ---------------------------------------------------------------------------
def create_instance(
db: Session,
*,
process_version_id: str,
business_type: Optional[str] = None,
business_id: Optional[str] = None,
starter_user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None,
status: Optional[str] = None,
callback_url: Optional[str] = None,
tenant_no: Optional[str] = None,
user_id: Optional[str] = None,
) -> WfProcessInstance:
obj = WfProcessInstance(
id=generate_uuid(),
process_version_id=process_version_id,
business_type=business_type,
business_id=business_id,
starter_user_id=starter_user_id,
variables_json=json.dumps(variables, ensure_ascii=False) if variables else None,
status=status or "running",
callback_url=callback_url,
tenant_no=tenant_no or "zkxw",
order_num=0,
remark=None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def get_instance(db: Session, instance_id: str) -> Optional[WfProcessInstance]:
return (
db.query(WfProcessInstance)
.filter(and_(WfProcessInstance.id == instance_id, WfProcessInstance.del_flag == 0))
.first()
)
def update_instance_status(db: Session, instance_id: str, status: str) -> None:
obj = get_instance(db, instance_id)
if not obj:
raise ValueError("流程实例不存在")
obj.status = status
obj.update_time = datetime.now()
db.add(obj)
db.commit()
def list_my_instances(
db: Session,
user_id: str,
*,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
status: Optional[str] = None,
tenant_no: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
from sqlalchemy import func as sa_func
latest_per_biz = (
db.query(
WfProcessInstance.business_id,
sa_func.max(WfProcessInstance.create_time).label("max_ct"),
)
.filter(
and_(
WfProcessInstance.starter_user_id == user_id,
WfProcessInstance.del_flag == 0,
WfProcessInstance.business_id != None,
WfProcessInstance.business_id != "",
)
)
.group_by(WfProcessInstance.business_id)
.subquery()
)
q0 = (
db.query(WfProcessInstance, WfProcessVersion, WfProcessDef)
.join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
.join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
.join(
latest_per_biz,
and_(
WfProcessInstance.business_id == latest_per_biz.c.business_id,
WfProcessInstance.create_time == latest_per_biz.c.max_ct,
),
)
.filter(
and_(
WfProcessInstance.starter_user_id == user_id,
WfProcessInstance.del_flag == 0,
WfProcessVersion.del_flag == 0,
WfProcessDef.del_flag == 0,
)
)
)
if tenant_no:
q0 = q0.filter(WfProcessInstance.tenant_no == tenant_no)
if status:
q0 = q0.filter(WfProcessInstance.status == status)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(
or_(WfProcessDef.process_name.like(like), WfProcessInstance.business_id.like(like))
)
if user_info:
from app.core.data_scope import apply_data_scope
q0 = apply_data_scope(q0, WfProcessInstance, user_info, db, tenant_no)
total = q0.order_by(None).with_entities(func.count(WfProcessInstance.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q0.order_by(WfProcessInstance.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
result = []
for inst, ver, defn in rows:
result.append({
"id": inst.id,
"processName": defn.process_name,
"businessType": inst.business_type,
"businessId": inst.business_id,
"starterUserId": inst.starter_user_id,
"status": inst.status,
"createTime": inst.create_time.strftime("%Y-%m-%d %H:%M:%S") if inst.create_time else "",
"updateTime": inst.update_time.strftime("%Y-%m-%d %H:%M:%S") if inst.update_time else "",
})
return result, int(total)
def get_all_tasks_for_business(
db: Session,
business_type: str,
business_id: str,
) -> List[Dict[str, Any]]:
instances = (
db.query(WfProcessInstance)
.filter(
and_(
WfProcessInstance.business_type == business_type,
WfProcessInstance.business_id == business_id,
WfProcessInstance.del_flag == 0,
)
)
.order_by(WfProcessInstance.create_time.asc())
.all()
)
all_tasks: List[Dict[str, Any]] = []
for inst in instances:
node_name_map: Dict[str, str] = {}
if inst.process_version_id:
for n in get_nodes_by_version(db, inst.process_version_id):
node_name_map[n.node_key] = n.node_name or n.node_key
tasks = (
db.query(WfTask)
.filter(and_(WfTask.instance_id == inst.id, WfTask.del_flag == 0))
.order_by(WfTask.create_time.asc())
.all()
)
user_ids = {t.assignee_user_id for t in tasks if t.assignee_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}
for t in tasks:
all_tasks.append({
"id": t.id,
"instanceId": inst.id,
"instanceStatus": inst.status,
"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 "",
})
return all_tasks
# ---------------------------------------------------------------------------
# 流程任务 wf_task
# ---------------------------------------------------------------------------
def create_task(
db: Session,
*,
instance_id: str,
node_key: Optional[str] = None,
assignee_user_id: Optional[str] = None,
tenant_no: Optional[str] = None,
user_id: Optional[str] = None,
) -> WfTask:
obj = WfTask(
id=generate_uuid(),
instance_id=instance_id,
node_key=node_key,
assignee_user_id=assignee_user_id,
status="pending",
tenant_no=tenant_no or "zkxw",
order_num=0,
remark=None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def get_task(db: Session, task_id: str) -> Optional[WfTask]:
return (
db.query(WfTask)
.filter(and_(WfTask.id == task_id, WfTask.del_flag == 0))
.first()
)
def get_pending_tasks_by_node(db: Session, instance_id: str, node_key: str) -> List[WfTask]:
return (
db.query(WfTask)
.filter(
and_(
WfTask.instance_id == instance_id,
WfTask.node_key == node_key,
WfTask.status == "pending",
WfTask.del_flag == 0,
)
)
.all()
)
def _task_row_to_dict(
t: WfTask,
process_name: Optional[str] = None,
business_type: Optional[str] = None,
business_id: Optional[str] = None,
starter_user_id: Optional[str] = None,
instance_status: Optional[str] = None,
instance_create_time: Optional[str] = None,
) -> Dict[str, Any]:
return {
"id": t.id,
"instanceId": t.instance_id,
"nodeKey": t.node_key,
"assigneeUserId": t.assignee_user_id,
"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 "",
"processName": process_name,
"businessType": business_type,
"businessId": business_id,
"starterUserId": starter_user_id,
"instanceStatus": instance_status,
"instanceCreateTime": instance_create_time,
}
def list_pending_tasks(
db: Session,
user_id: str,
*,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
tenant_no: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
q0 = (
db.query(WfTask, WfProcessInstance, WfProcessVersion, WfProcessDef)
.join(WfProcessInstance, WfTask.instance_id == WfProcessInstance.id)
.join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
.join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
.filter(
and_(
WfTask.assignee_user_id == user_id,
WfTask.status == "pending",
WfTask.del_flag == 0,
WfProcessInstance.del_flag == 0,
WfProcessVersion.del_flag == 0,
WfProcessDef.del_flag == 0,
)
)
)
if tenant_no:
q0 = q0.filter(WfTask.tenant_no == tenant_no)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(
or_(WfProcessDef.process_name.like(like), WfProcessDef.process_code.like(like))
)
if user_info:
from app.core.data_scope import apply_data_scope
q0 = apply_data_scope(q0, WfTask, user_info, db, tenant_no)
total = q0.order_by(None).with_entities(func.count(WfTask.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q0.order_by(WfTask.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
result = []
for task, inst, ver, defn in rows:
result.append(
_task_row_to_dict(
task,
process_name=defn.process_name,
business_type=inst.business_type,
business_id=inst.business_id,
starter_user_id=inst.starter_user_id,
instance_status=inst.status,
instance_create_time=inst.create_time.strftime("%Y-%m-%d %H:%M:%S") if inst.create_time else "",
)
)
return result, int(total)
def list_completed_tasks(
db: Session,
user_id: str,
*,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
tenant_no: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
q0 = (
db.query(WfTask, WfProcessInstance, WfProcessVersion, WfProcessDef)
.join(WfProcessInstance, WfTask.instance_id == WfProcessInstance.id)
.join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
.join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
.filter(
and_(
WfTask.assignee_user_id == user_id,
WfTask.status.in_(["approved", "rejected"]),
WfTask.del_flag == 0,
WfProcessInstance.del_flag == 0,
WfProcessVersion.del_flag == 0,
WfProcessDef.del_flag == 0,
)
)
)
if tenant_no:
q0 = q0.filter(WfTask.tenant_no == tenant_no)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(
or_(WfProcessDef.process_name.like(like), WfProcessDef.process_code.like(like))
)
if user_info:
from app.core.data_scope import apply_data_scope
q0 = apply_data_scope(q0, WfTask, user_info, db, tenant_no)
total = q0.order_by(None).with_entities(func.count(WfTask.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q0.order_by(WfTask.update_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
result = []
for task, inst, ver, defn in rows:
result.append(
_task_row_to_dict(
task,
process_name=defn.process_name,
business_type=inst.business_type,
business_id=inst.business_id,
starter_user_id=inst.starter_user_id,
instance_status=inst.status,
instance_create_time=inst.create_time.strftime("%Y-%m-%d %H:%M:%S") if inst.create_time else "",
)
)
return result, int(total)
def total_pages(total: int, page_size: int) -> int:
if total <= 0:
return 0
return int(math.ceil(total / float(page_size)))