workflow.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910
  1. """工作流 CRUD(wf_process_def / wf_process_version / wf_node / wf_transition / wf_process_instance / wf_task)"""
  2. from __future__ import annotations
  3. import json
  4. import math
  5. from datetime import datetime
  6. from typing import Any, Dict, List, Optional, Tuple
  7. from sqlalchemy import and_, func, or_
  8. from sqlalchemy.orm import Session
  9. from app.core.delete_guard import assert_row_deletable_by_creator
  10. from app.core.utils import generate_uuid
  11. from app.models.models import (
  12. SysUser,
  13. WfNode,
  14. WfProcessDef,
  15. WfProcessInstance,
  16. WfProcessVersion,
  17. WfTask,
  18. WfTransition,
  19. )
  20. # ---------------------------------------------------------------------------
  21. # 流程定义 wf_process_def
  22. # ---------------------------------------------------------------------------
  23. def _row_to_dict(t: WfProcessDef) -> Dict[str, Any]:
  24. return {
  25. "id": t.id,
  26. "processCode": t.process_code,
  27. "processName": t.process_name,
  28. "status": t.status,
  29. "tenantNo": t.tenant_no,
  30. "orderNum": t.order_num if t.order_num is not None else 0,
  31. "remark": t.remark or "",
  32. "createBy": t.create_by or "",
  33. "createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
  34. "updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
  35. }
  36. def list_process_defs(
  37. db: Session,
  38. *,
  39. page: int = 1,
  40. page_size: int = 10,
  41. keyword: Optional[str] = None,
  42. status: Optional[str] = None,
  43. tenant_no: Optional[str] = None,
  44. user_info=None,
  45. ) -> Tuple[List[Dict[str, Any]], int]:
  46. def _filtered():
  47. q0 = db.query(WfProcessDef).filter(WfProcessDef.del_flag == 0)
  48. if tenant_no:
  49. q0 = q0.filter(WfProcessDef.tenant_no == tenant_no)
  50. if status is not None:
  51. q0 = q0.filter(WfProcessDef.status == status)
  52. kw = (keyword or "").strip()
  53. if kw:
  54. like = f"%{kw}%"
  55. q0 = q0.filter(or_(WfProcessDef.process_name.like(like), WfProcessDef.process_code.like(like)))
  56. if user_info:
  57. from app.core.data_scope import apply_data_scope
  58. q0 = apply_data_scope(q0, WfProcessDef, user_info, db, tenant_no)
  59. return q0
  60. total = _filtered().order_by(None).with_entities(func.count(WfProcessDef.id)).scalar() or 0
  61. page = max(1, page)
  62. page_size = min(max(1, page_size), 500)
  63. rows = (
  64. _filtered()
  65. .order_by(WfProcessDef.create_time.desc())
  66. .offset((page - 1) * page_size)
  67. .limit(page_size)
  68. .all()
  69. )
  70. result = []
  71. for r in rows:
  72. d = _row_to_dict(r)
  73. ver = (
  74. db.query(WfProcessVersion)
  75. .filter(
  76. and_(
  77. WfProcessVersion.process_def_id == r.id,
  78. WfProcessVersion.is_latest == "1",
  79. WfProcessVersion.del_flag == 0,
  80. )
  81. )
  82. .order_by(WfProcessVersion.version_no.desc())
  83. .first()
  84. )
  85. d["latestVersion"] = ver.version_no if ver else None
  86. result.append(d)
  87. return result, int(total)
  88. def get_process_def(db: Session, def_id: str) -> Optional[WfProcessDef]:
  89. return (
  90. db.query(WfProcessDef)
  91. .filter(and_(WfProcessDef.id == def_id, WfProcessDef.del_flag == 0))
  92. .first()
  93. )
  94. def process_code_exists(
  95. db: Session, code: str, tenant_no: str, exclude_id: Optional[str] = None
  96. ) -> bool:
  97. q = db.query(WfProcessDef).filter(
  98. and_(WfProcessDef.process_code == code.strip(), WfProcessDef.tenant_no == tenant_no, WfProcessDef.del_flag == 0)
  99. )
  100. if exclude_id:
  101. q = q.filter(WfProcessDef.id != exclude_id)
  102. return q.first() is not None
  103. def create_process_def(
  104. db: Session,
  105. *,
  106. process_code: str,
  107. process_name: str,
  108. tenant_no: str,
  109. status: Optional[str] = None,
  110. order_num: Optional[int] = None,
  111. remark: Optional[str] = None,
  112. user_id: Optional[str] = None,
  113. ) -> WfProcessDef:
  114. code = (process_code or "").strip()
  115. if not code:
  116. raise ValueError("流程编码不能为空")
  117. if process_code_exists(db, code, tenant_no):
  118. raise ValueError("流程编码已存在")
  119. obj = WfProcessDef(
  120. id=generate_uuid(),
  121. process_code=code,
  122. process_name=(process_name or "").strip(),
  123. status=status or "1",
  124. tenant_no=tenant_no,
  125. order_num=order_num if order_num is not None else 0,
  126. remark=(remark or "").strip() or None,
  127. create_by=user_id,
  128. create_dept_id="",
  129. create_time=datetime.now(),
  130. update_by=user_id,
  131. update_time=datetime.now(),
  132. del_flag=0,
  133. )
  134. db.add(obj)
  135. db.commit()
  136. db.refresh(obj)
  137. return obj
  138. def update_process_def(
  139. db: Session,
  140. def_id: str,
  141. *,
  142. process_code: Optional[str] = None,
  143. process_name: Optional[str] = None,
  144. status: Optional[str] = None,
  145. order_num: Optional[int] = None,
  146. remark: Optional[str] = None,
  147. user_id: Optional[str] = None,
  148. tenant_no: Optional[str] = None,
  149. ) -> WfProcessDef:
  150. obj = get_process_def(db, def_id)
  151. if not obj:
  152. raise ValueError("流程定义不存在")
  153. if process_code is not None:
  154. new_code = process_code.strip()
  155. if not new_code:
  156. raise ValueError("流程编码不能为空")
  157. tn = tenant_no or obj.tenant_no
  158. if new_code != obj.process_code and process_code_exists(db, new_code, tn, exclude_id=def_id):
  159. raise ValueError("流程编码已存在")
  160. obj.process_code = new_code
  161. if process_name is not None:
  162. obj.process_name = (process_name or "").strip()
  163. if status is not None:
  164. obj.status = status
  165. if order_num is not None:
  166. obj.order_num = order_num
  167. if remark is not None:
  168. obj.remark = (remark or "").strip() or None
  169. obj.update_by = user_id
  170. obj.update_time = datetime.now()
  171. db.add(obj)
  172. db.commit()
  173. db.refresh(obj)
  174. return obj
  175. def soft_delete_process_def(db: Session, def_id: str, *, user_info: dict = None) -> None:
  176. obj = get_process_def(db, def_id)
  177. if not obj:
  178. raise ValueError("流程定义不存在")
  179. assert_row_deletable_by_creator(obj, user_info)
  180. obj.del_flag = 1
  181. obj.update_time = datetime.now()
  182. db.add(obj)
  183. db.commit()
  184. # ---------------------------------------------------------------------------
  185. # 流程版本 wf_process_version
  186. # ---------------------------------------------------------------------------
  187. def publish_version(
  188. db: Session,
  189. process_def_id: str,
  190. graph_json: str,
  191. nodes: List[Dict[str, Any]],
  192. transitions: List[Dict[str, Any]],
  193. tenant_no: str,
  194. remark: Optional[str] = None,
  195. user_id: Optional[str] = None,
  196. ) -> WfProcessVersion:
  197. defn = get_process_def(db, process_def_id)
  198. if not defn:
  199. raise ValueError("流程定义不存在")
  200. db.query(WfProcessVersion).filter(
  201. and_(
  202. WfProcessVersion.process_def_id == process_def_id,
  203. WfProcessVersion.is_latest == "1",
  204. WfProcessVersion.del_flag == 0,
  205. )
  206. ).update({"is_latest": "0", "update_time": datetime.now()}, synchronize_session=False)
  207. max_ver = (
  208. db.query(func.max(WfProcessVersion.version_no))
  209. .filter(and_(WfProcessVersion.process_def_id == process_def_id, WfProcessVersion.del_flag == 0))
  210. .scalar()
  211. ) or 0
  212. new_ver_no = max_ver + 1
  213. ver = WfProcessVersion(
  214. id=generate_uuid(),
  215. process_def_id=process_def_id,
  216. version_no=new_ver_no,
  217. graph_json=graph_json,
  218. is_latest="1",
  219. tenant_no=tenant_no,
  220. order_num=0,
  221. remark=(remark or "").strip() or None,
  222. create_by=user_id,
  223. create_dept_id="",
  224. create_time=datetime.now(),
  225. update_by=user_id,
  226. update_time=datetime.now(),
  227. del_flag=0,
  228. )
  229. db.add(ver)
  230. db.flush()
  231. for idx, n in enumerate(nodes):
  232. node = WfNode(
  233. id=generate_uuid(),
  234. process_version_id=ver.id,
  235. node_key=n.get("nodeKey", ""),
  236. node_type=n.get("nodeType", ""),
  237. node_name=n.get("nodeName"),
  238. config_json=n.get("configJson"),
  239. tenant_no=tenant_no,
  240. order_num=idx,
  241. remark=None,
  242. create_by=user_id,
  243. create_dept_id="",
  244. create_time=datetime.now(),
  245. update_by=user_id,
  246. update_time=datetime.now(),
  247. del_flag=0,
  248. )
  249. db.add(node)
  250. for idx, t in enumerate(transitions):
  251. trans = WfTransition(
  252. id=generate_uuid(),
  253. process_version_id=ver.id,
  254. from_node_key=t.get("fromNodeKey", ""),
  255. to_node_key=t.get("toNodeKey", ""),
  256. condition_expr=t.get("conditionExpr"),
  257. tenant_no=tenant_no,
  258. order_num=idx,
  259. remark=None,
  260. create_by=user_id,
  261. create_dept_id="",
  262. create_time=datetime.now(),
  263. update_by=user_id,
  264. update_time=datetime.now(),
  265. del_flag=0,
  266. )
  267. db.add(trans)
  268. db.commit()
  269. db.refresh(ver)
  270. return ver
  271. def get_latest_version(db: Session, process_def_id: str) -> Optional[WfProcessVersion]:
  272. return (
  273. db.query(WfProcessVersion)
  274. .filter(
  275. and_(
  276. WfProcessVersion.process_def_id == process_def_id,
  277. WfProcessVersion.is_latest == "1",
  278. WfProcessVersion.del_flag == 0,
  279. )
  280. )
  281. .first()
  282. )
  283. def _version_to_dict(v: WfProcessVersion) -> Dict[str, Any]:
  284. return {
  285. "id": v.id,
  286. "versionNo": v.version_no,
  287. "isLatest": v.is_latest,
  288. "remark": v.remark or "",
  289. "createTime": v.create_time.strftime("%Y-%m-%d %H:%M:%S") if v.create_time else "",
  290. "updateTime": v.update_time.strftime("%Y-%m-%d %H:%M:%S") if v.update_time else "",
  291. }
  292. def list_versions(
  293. db: Session, process_def_id: str, tenant_no: Optional[str] = None, user_info=None
  294. ) -> List[Dict[str, Any]]:
  295. q = db.query(WfProcessVersion).filter(
  296. and_(WfProcessVersion.process_def_id == process_def_id, WfProcessVersion.del_flag == 0)
  297. )
  298. if tenant_no:
  299. q = q.filter(WfProcessVersion.tenant_no == tenant_no)
  300. if user_info:
  301. from app.core.data_scope import apply_data_scope
  302. q = apply_data_scope(q, WfProcessVersion, user_info, db, tenant_no)
  303. rows = q.order_by(WfProcessVersion.version_no.desc()).all()
  304. return [_version_to_dict(v) for v in rows]
  305. def get_nodes_by_version(db: Session, version_id: str) -> List[WfNode]:
  306. return (
  307. db.query(WfNode)
  308. .filter(and_(WfNode.process_version_id == version_id, WfNode.del_flag == 0))
  309. .order_by(WfNode.order_num.asc())
  310. .all()
  311. )
  312. def get_transitions_by_version(db: Session, version_id: str) -> List[WfTransition]:
  313. return (
  314. db.query(WfTransition)
  315. .filter(and_(WfTransition.process_version_id == version_id, WfTransition.del_flag == 0))
  316. .order_by(WfTransition.order_num.asc())
  317. .all()
  318. )
  319. # ---------------------------------------------------------------------------
  320. # 流程实例 wf_process_instance
  321. # ---------------------------------------------------------------------------
  322. def create_instance(
  323. db: Session,
  324. *,
  325. process_version_id: str,
  326. business_type: Optional[str] = None,
  327. business_id: Optional[str] = None,
  328. starter_user_id: Optional[str] = None,
  329. variables: Optional[Dict[str, Any]] = None,
  330. status: Optional[str] = None,
  331. callback_url: Optional[str] = None,
  332. tenant_no: Optional[str] = None,
  333. user_id: Optional[str] = None,
  334. ) -> WfProcessInstance:
  335. obj = WfProcessInstance(
  336. id=generate_uuid(),
  337. process_version_id=process_version_id,
  338. business_type=business_type,
  339. business_id=business_id,
  340. starter_user_id=starter_user_id,
  341. variables_json=json.dumps(variables, ensure_ascii=False) if variables else None,
  342. status=status or "running",
  343. callback_url=callback_url,
  344. tenant_no=tenant_no or "shanxia2.0",
  345. order_num=0,
  346. remark=None,
  347. create_by=user_id,
  348. create_dept_id="",
  349. create_time=datetime.now(),
  350. update_by=user_id,
  351. update_time=datetime.now(),
  352. del_flag=0,
  353. )
  354. db.add(obj)
  355. db.commit()
  356. db.refresh(obj)
  357. return obj
  358. def get_instance(db: Session, instance_id: str) -> Optional[WfProcessInstance]:
  359. return (
  360. db.query(WfProcessInstance)
  361. .filter(and_(WfProcessInstance.id == instance_id, WfProcessInstance.del_flag == 0))
  362. .first()
  363. )
  364. def update_instance_status(db: Session, instance_id: str, status: str) -> None:
  365. obj = get_instance(db, instance_id)
  366. if not obj:
  367. raise ValueError("流程实例不存在")
  368. obj.status = status
  369. obj.update_time = datetime.now()
  370. db.add(obj)
  371. db.commit()
  372. def _apply_workflow_list_keyword(query, keyword: Optional[str], *, scope: str):
  373. """列表与流程名称候选值按同一 scope 匹配完整关键词字段。"""
  374. kw = (keyword or "").strip()
  375. if not kw:
  376. return query
  377. like = f"%{kw}%"
  378. secondary_column = (
  379. WfProcessInstance.business_id if scope == "mine" else WfProcessDef.process_code
  380. )
  381. return query.filter(
  382. or_(WfProcessDef.process_name.like(like), secondary_column.like(like))
  383. )
  384. def list_my_instances(
  385. db: Session,
  386. user_id: str,
  387. *,
  388. page: int = 1,
  389. page_size: int = 10,
  390. keyword: Optional[str] = None,
  391. status: Optional[str] = None,
  392. process_name_in: Optional[str] = None,
  393. tenant_no: Optional[str] = None,
  394. user_info=None,
  395. ) -> Tuple[List[Dict[str, Any]], int]:
  396. from sqlalchemy import func as sa_func
  397. latest_per_biz = (
  398. db.query(
  399. WfProcessInstance.business_id,
  400. sa_func.max(WfProcessInstance.create_time).label("max_ct"),
  401. )
  402. .filter(
  403. and_(
  404. WfProcessInstance.starter_user_id == user_id,
  405. WfProcessInstance.del_flag == 0,
  406. WfProcessInstance.business_id != None,
  407. WfProcessInstance.business_id != "",
  408. )
  409. )
  410. .group_by(WfProcessInstance.business_id)
  411. .subquery()
  412. )
  413. q0 = (
  414. db.query(WfProcessInstance, WfProcessVersion, WfProcessDef)
  415. .join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
  416. .join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
  417. .join(
  418. latest_per_biz,
  419. and_(
  420. WfProcessInstance.business_id == latest_per_biz.c.business_id,
  421. WfProcessInstance.create_time == latest_per_biz.c.max_ct,
  422. ),
  423. )
  424. .filter(
  425. and_(
  426. WfProcessInstance.starter_user_id == user_id,
  427. WfProcessInstance.del_flag == 0,
  428. WfProcessVersion.del_flag == 0,
  429. WfProcessDef.del_flag == 0,
  430. )
  431. )
  432. )
  433. if tenant_no:
  434. q0 = q0.filter(WfProcessInstance.tenant_no == tenant_no)
  435. if status:
  436. q0 = q0.filter(WfProcessInstance.status == status)
  437. q0 = _apply_workflow_list_keyword(q0, keyword, scope="mine")
  438. pn_in = [s.strip() for s in str(process_name_in or "").split(",") if s.strip()]
  439. if pn_in:
  440. q0 = q0.filter(WfProcessDef.process_name.in_(pn_in))
  441. if user_info:
  442. from app.core.data_scope import apply_data_scope
  443. q0 = apply_data_scope(q0, WfProcessInstance, user_info, db, tenant_no)
  444. total = q0.order_by(None).with_entities(func.count(WfProcessInstance.id)).scalar() or 0
  445. page = max(1, page)
  446. page_size = min(max(1, page_size), 500)
  447. rows = (
  448. q0.order_by(WfProcessInstance.create_time.desc())
  449. .offset((page - 1) * page_size)
  450. .limit(page_size)
  451. .all()
  452. )
  453. result = []
  454. for inst, ver, defn in rows:
  455. result.append({
  456. "id": inst.id,
  457. "processName": defn.process_name,
  458. "businessType": inst.business_type,
  459. "businessId": inst.business_id,
  460. "starterUserId": inst.starter_user_id,
  461. "status": inst.status,
  462. "createTime": inst.create_time.strftime("%Y-%m-%d %H:%M:%S") if inst.create_time else "",
  463. "updateTime": inst.update_time.strftime("%Y-%m-%d %H:%M:%S") if inst.update_time else "",
  464. })
  465. return result, int(total)
  466. def get_all_tasks_for_business(
  467. db: Session,
  468. business_type: str,
  469. business_id: str,
  470. ) -> List[Dict[str, Any]]:
  471. instances = (
  472. db.query(WfProcessInstance)
  473. .filter(
  474. and_(
  475. WfProcessInstance.business_type == business_type,
  476. WfProcessInstance.business_id == business_id,
  477. WfProcessInstance.del_flag == 0,
  478. )
  479. )
  480. .order_by(WfProcessInstance.create_time.asc())
  481. .all()
  482. )
  483. all_tasks: List[Dict[str, Any]] = []
  484. for inst in instances:
  485. node_name_map: Dict[str, str] = {}
  486. if inst.process_version_id:
  487. for n in get_nodes_by_version(db, inst.process_version_id):
  488. node_name_map[n.node_key] = n.node_name or n.node_key
  489. tasks = (
  490. db.query(WfTask)
  491. .filter(and_(WfTask.instance_id == inst.id, WfTask.del_flag == 0))
  492. .order_by(WfTask.create_time.asc())
  493. .all()
  494. )
  495. user_ids = {t.assignee_user_id for t in tasks if t.assignee_user_id}
  496. user_name_map: Dict[str, str] = {}
  497. if user_ids:
  498. rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(user_ids)).all()
  499. user_name_map = {r.id: r.nick_name for r in rows}
  500. for t in tasks:
  501. all_tasks.append({
  502. "id": t.id,
  503. "instanceId": inst.id,
  504. "instanceStatus": inst.status,
  505. "nodeKey": t.node_key,
  506. "nodeName": node_name_map.get(t.node_key, t.node_key or ""),
  507. "assigneeUserId": t.assignee_user_id,
  508. "assigneeUserName": user_name_map.get(t.assignee_user_id, t.assignee_user_id or ""),
  509. "status": t.status,
  510. "commentText": t.comment_text,
  511. "createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
  512. "updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
  513. })
  514. return all_tasks
  515. # ---------------------------------------------------------------------------
  516. # 流程任务 wf_task
  517. # ---------------------------------------------------------------------------
  518. def create_task(
  519. db: Session,
  520. *,
  521. instance_id: str,
  522. node_key: Optional[str] = None,
  523. assignee_user_id: Optional[str] = None,
  524. tenant_no: Optional[str] = None,
  525. user_id: Optional[str] = None,
  526. ) -> WfTask:
  527. obj = WfTask(
  528. id=generate_uuid(),
  529. instance_id=instance_id,
  530. node_key=node_key,
  531. assignee_user_id=assignee_user_id,
  532. status="pending",
  533. tenant_no=tenant_no or "shanxia2.0",
  534. order_num=0,
  535. remark=None,
  536. create_by=user_id,
  537. create_dept_id="",
  538. create_time=datetime.now(),
  539. update_by=user_id,
  540. update_time=datetime.now(),
  541. del_flag=0,
  542. )
  543. db.add(obj)
  544. db.commit()
  545. db.refresh(obj)
  546. return obj
  547. def get_task(db: Session, task_id: str) -> Optional[WfTask]:
  548. return (
  549. db.query(WfTask)
  550. .filter(and_(WfTask.id == task_id, WfTask.del_flag == 0))
  551. .first()
  552. )
  553. def get_pending_tasks_by_node(db: Session, instance_id: str, node_key: str) -> List[WfTask]:
  554. return (
  555. db.query(WfTask)
  556. .filter(
  557. and_(
  558. WfTask.instance_id == instance_id,
  559. WfTask.node_key == node_key,
  560. WfTask.status == "pending",
  561. WfTask.del_flag == 0,
  562. )
  563. )
  564. .all()
  565. )
  566. def _task_row_to_dict(
  567. t: WfTask,
  568. process_name: Optional[str] = None,
  569. business_type: Optional[str] = None,
  570. business_id: Optional[str] = None,
  571. starter_user_id: Optional[str] = None,
  572. instance_status: Optional[str] = None,
  573. instance_create_time: Optional[str] = None,
  574. ) -> Dict[str, Any]:
  575. return {
  576. "id": t.id,
  577. "instanceId": t.instance_id,
  578. "nodeKey": t.node_key,
  579. "assigneeUserId": t.assignee_user_id,
  580. "status": t.status,
  581. "commentText": t.comment_text,
  582. "createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
  583. "updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
  584. "processName": process_name,
  585. "businessType": business_type,
  586. "businessId": business_id,
  587. "starterUserId": starter_user_id,
  588. "instanceStatus": instance_status,
  589. "instanceCreateTime": instance_create_time,
  590. }
  591. def list_pending_tasks(
  592. db: Session,
  593. user_id: str,
  594. *,
  595. page: int = 1,
  596. page_size: int = 10,
  597. keyword: Optional[str] = None,
  598. process_name_in: Optional[str] = None,
  599. tenant_no: Optional[str] = None,
  600. user_info=None,
  601. ) -> Tuple[List[Dict[str, Any]], int]:
  602. q0 = (
  603. db.query(WfTask, WfProcessInstance, WfProcessVersion, WfProcessDef)
  604. .join(WfProcessInstance, WfTask.instance_id == WfProcessInstance.id)
  605. .join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
  606. .join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
  607. .filter(
  608. and_(
  609. WfTask.assignee_user_id == user_id,
  610. WfTask.status == "pending",
  611. WfTask.del_flag == 0,
  612. WfProcessInstance.del_flag == 0,
  613. WfProcessVersion.del_flag == 0,
  614. WfProcessDef.del_flag == 0,
  615. )
  616. )
  617. )
  618. if tenant_no:
  619. q0 = q0.filter(WfTask.tenant_no == tenant_no)
  620. q0 = _apply_workflow_list_keyword(q0, keyword, scope="pending")
  621. pn_in = [s.strip() for s in str(process_name_in or "").split(",") if s.strip()]
  622. if pn_in:
  623. q0 = q0.filter(WfProcessDef.process_name.in_(pn_in))
  624. if user_info:
  625. from app.core.data_scope import apply_data_scope
  626. q0 = apply_data_scope(q0, WfTask, user_info, db, tenant_no)
  627. total = q0.order_by(None).with_entities(func.count(WfTask.id)).scalar() or 0
  628. page = max(1, page)
  629. page_size = min(max(1, page_size), 500)
  630. rows = (
  631. q0.order_by(WfTask.create_time.desc())
  632. .offset((page - 1) * page_size)
  633. .limit(page_size)
  634. .all()
  635. )
  636. result = []
  637. for task, inst, ver, defn in rows:
  638. result.append(
  639. _task_row_to_dict(
  640. task,
  641. process_name=defn.process_name,
  642. business_type=inst.business_type,
  643. business_id=inst.business_id,
  644. starter_user_id=inst.starter_user_id,
  645. instance_status=inst.status,
  646. instance_create_time=inst.create_time.strftime("%Y-%m-%d %H:%M:%S") if inst.create_time else "",
  647. )
  648. )
  649. return result, int(total)
  650. def list_completed_tasks(
  651. db: Session,
  652. user_id: str,
  653. *,
  654. page: int = 1,
  655. page_size: int = 10,
  656. keyword: Optional[str] = None,
  657. process_name_in: Optional[str] = None,
  658. tenant_no: Optional[str] = None,
  659. user_info=None,
  660. ) -> Tuple[List[Dict[str, Any]], int]:
  661. q0 = (
  662. db.query(WfTask, WfProcessInstance, WfProcessVersion, WfProcessDef)
  663. .join(WfProcessInstance, WfTask.instance_id == WfProcessInstance.id)
  664. .join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
  665. .join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
  666. .filter(
  667. and_(
  668. WfTask.assignee_user_id == user_id,
  669. WfTask.status.in_(["approved", "rejected"]),
  670. WfTask.del_flag == 0,
  671. WfProcessInstance.del_flag == 0,
  672. WfProcessVersion.del_flag == 0,
  673. WfProcessDef.del_flag == 0,
  674. )
  675. )
  676. )
  677. if tenant_no:
  678. q0 = q0.filter(WfTask.tenant_no == tenant_no)
  679. q0 = _apply_workflow_list_keyword(q0, keyword, scope="completed")
  680. pn_in = [s.strip() for s in str(process_name_in or "").split(",") if s.strip()]
  681. if pn_in:
  682. q0 = q0.filter(WfProcessDef.process_name.in_(pn_in))
  683. if user_info:
  684. from app.core.data_scope import apply_data_scope
  685. q0 = apply_data_scope(q0, WfTask, user_info, db, tenant_no)
  686. total = q0.order_by(None).with_entities(func.count(WfTask.id)).scalar() or 0
  687. page = max(1, page)
  688. page_size = min(max(1, page_size), 500)
  689. rows = (
  690. q0.order_by(WfTask.update_time.desc())
  691. .offset((page - 1) * page_size)
  692. .limit(page_size)
  693. .all()
  694. )
  695. result = []
  696. for task, inst, ver, defn in rows:
  697. result.append(
  698. _task_row_to_dict(
  699. task,
  700. process_name=defn.process_name,
  701. business_type=inst.business_type,
  702. business_id=inst.business_id,
  703. starter_user_id=inst.starter_user_id,
  704. instance_status=inst.status,
  705. instance_create_time=inst.create_time.strftime("%Y-%m-%d %H:%M:%S") if inst.create_time else "",
  706. )
  707. )
  708. return result, int(total)
  709. def facet_process_names(
  710. db: Session,
  711. user_id: str,
  712. *,
  713. scope: str,
  714. keyword: Optional[str] = None,
  715. limit: int = 500,
  716. tenant_no: Optional[str] = None,
  717. user_info=None,
  718. status: Optional[str] = None,
  719. status_in: Optional[str] = None,
  720. ) -> List[Dict[str, Any]]:
  721. """任务/申请列表列头可选值:按 scope 复用对应列表的基础查询,对流程名称去重计数。"""
  722. from sqlalchemy import func as sa_func
  723. if scope == "mine":
  724. latest_per_biz = (
  725. db.query(
  726. WfProcessInstance.business_id,
  727. sa_func.max(WfProcessInstance.create_time).label("max_ct"),
  728. )
  729. .filter(
  730. and_(
  731. WfProcessInstance.starter_user_id == user_id,
  732. WfProcessInstance.del_flag == 0,
  733. WfProcessInstance.business_id != None,
  734. WfProcessInstance.business_id != "",
  735. )
  736. )
  737. .group_by(WfProcessInstance.business_id)
  738. .subquery()
  739. )
  740. q = (
  741. db.query(
  742. WfProcessDef.process_name.label("value"),
  743. func.count(WfProcessInstance.id).label("cnt"),
  744. )
  745. .select_from(WfProcessInstance)
  746. .join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
  747. .join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
  748. .join(
  749. latest_per_biz,
  750. and_(
  751. WfProcessInstance.business_id == latest_per_biz.c.business_id,
  752. WfProcessInstance.create_time == latest_per_biz.c.max_ct,
  753. ),
  754. )
  755. .filter(
  756. and_(
  757. WfProcessInstance.starter_user_id == user_id,
  758. WfProcessInstance.del_flag == 0,
  759. WfProcessVersion.del_flag == 0,
  760. WfProcessDef.del_flag == 0,
  761. )
  762. )
  763. )
  764. if tenant_no:
  765. q = q.filter(WfProcessInstance.tenant_no == tenant_no)
  766. if user_info:
  767. from app.core.data_scope import apply_data_scope
  768. q = apply_data_scope(q, WfProcessInstance, user_info, db, tenant_no)
  769. else:
  770. if scope == "completed":
  771. task_status = WfTask.status.in_(["approved", "rejected"])
  772. else:
  773. task_status = WfTask.status == "pending"
  774. q = (
  775. db.query(
  776. WfProcessDef.process_name.label("value"),
  777. func.count(WfTask.id).label("cnt"),
  778. )
  779. .select_from(WfTask)
  780. .join(WfProcessInstance, WfTask.instance_id == WfProcessInstance.id)
  781. .join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
  782. .join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
  783. .filter(
  784. and_(
  785. WfTask.assignee_user_id == user_id,
  786. task_status,
  787. WfTask.del_flag == 0,
  788. WfProcessInstance.del_flag == 0,
  789. WfProcessVersion.del_flag == 0,
  790. WfProcessDef.del_flag == 0,
  791. )
  792. )
  793. )
  794. if tenant_no:
  795. q = q.filter(WfTask.tenant_no == tenant_no)
  796. if user_info:
  797. from app.core.data_scope import apply_data_scope
  798. q = apply_data_scope(q, WfTask, user_info, db, tenant_no)
  799. q = q.filter(
  800. WfProcessDef.process_name.isnot(None),
  801. WfProcessDef.process_name != "",
  802. )
  803. status_values = []
  804. if status_in and str(status_in).strip():
  805. status_values = [s.strip() for s in str(status_in).split(",") if s.strip()]
  806. elif status and str(status).strip():
  807. status_values = [str(status).strip()]
  808. if status_values and scope == "mine":
  809. q = q.filter(WfProcessInstance.status.in_(status_values))
  810. q = _apply_workflow_list_keyword(q, keyword, scope=scope)
  811. lim = min(max(1, int(limit or 500)), 500)
  812. rows = q.group_by(WfProcessDef.process_name).order_by(WfProcessDef.process_name.asc()).limit(lim).all()
  813. out: List[Dict[str, Any]] = []
  814. for row in rows:
  815. value = str(row.value).strip() if row.value is not None else ""
  816. if not value:
  817. continue
  818. out.append({"value": value, "label": value, "count": int(row.cnt or 0)})
  819. return out
  820. def total_pages(total: int, page_size: int) -> int:
  821. if total <= 0:
  822. return 0
  823. return int(math.ceil(total / float(page_size)))