workflow.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  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), 100)
  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 list_my_instances(
  373. db: Session,
  374. user_id: str,
  375. *,
  376. page: int = 1,
  377. page_size: int = 10,
  378. keyword: Optional[str] = None,
  379. status: Optional[str] = None,
  380. tenant_no: Optional[str] = None,
  381. user_info=None,
  382. ) -> Tuple[List[Dict[str, Any]], int]:
  383. from sqlalchemy import func as sa_func
  384. latest_per_biz = (
  385. db.query(
  386. WfProcessInstance.business_id,
  387. sa_func.max(WfProcessInstance.create_time).label("max_ct"),
  388. )
  389. .filter(
  390. and_(
  391. WfProcessInstance.starter_user_id == user_id,
  392. WfProcessInstance.del_flag == 0,
  393. WfProcessInstance.business_id != None,
  394. WfProcessInstance.business_id != "",
  395. )
  396. )
  397. .group_by(WfProcessInstance.business_id)
  398. .subquery()
  399. )
  400. q0 = (
  401. db.query(WfProcessInstance, WfProcessVersion, WfProcessDef)
  402. .join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
  403. .join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
  404. .join(
  405. latest_per_biz,
  406. and_(
  407. WfProcessInstance.business_id == latest_per_biz.c.business_id,
  408. WfProcessInstance.create_time == latest_per_biz.c.max_ct,
  409. ),
  410. )
  411. .filter(
  412. and_(
  413. WfProcessInstance.starter_user_id == user_id,
  414. WfProcessInstance.del_flag == 0,
  415. WfProcessVersion.del_flag == 0,
  416. WfProcessDef.del_flag == 0,
  417. )
  418. )
  419. )
  420. if tenant_no:
  421. q0 = q0.filter(WfProcessInstance.tenant_no == tenant_no)
  422. if status:
  423. q0 = q0.filter(WfProcessInstance.status == status)
  424. kw = (keyword or "").strip()
  425. if kw:
  426. like = f"%{kw}%"
  427. q0 = q0.filter(
  428. or_(WfProcessDef.process_name.like(like), WfProcessInstance.business_id.like(like))
  429. )
  430. if user_info:
  431. from app.core.data_scope import apply_data_scope
  432. q0 = apply_data_scope(q0, WfProcessInstance, user_info, db, tenant_no)
  433. total = q0.order_by(None).with_entities(func.count(WfProcessInstance.id)).scalar() or 0
  434. page = max(1, page)
  435. page_size = min(max(1, page_size), 100)
  436. rows = (
  437. q0.order_by(WfProcessInstance.create_time.desc())
  438. .offset((page - 1) * page_size)
  439. .limit(page_size)
  440. .all()
  441. )
  442. result = []
  443. for inst, ver, defn in rows:
  444. result.append({
  445. "id": inst.id,
  446. "processName": defn.process_name,
  447. "businessType": inst.business_type,
  448. "businessId": inst.business_id,
  449. "starterUserId": inst.starter_user_id,
  450. "status": inst.status,
  451. "createTime": inst.create_time.strftime("%Y-%m-%d %H:%M:%S") if inst.create_time else "",
  452. "updateTime": inst.update_time.strftime("%Y-%m-%d %H:%M:%S") if inst.update_time else "",
  453. })
  454. return result, int(total)
  455. def get_all_tasks_for_business(
  456. db: Session,
  457. business_type: str,
  458. business_id: str,
  459. ) -> List[Dict[str, Any]]:
  460. instances = (
  461. db.query(WfProcessInstance)
  462. .filter(
  463. and_(
  464. WfProcessInstance.business_type == business_type,
  465. WfProcessInstance.business_id == business_id,
  466. WfProcessInstance.del_flag == 0,
  467. )
  468. )
  469. .order_by(WfProcessInstance.create_time.asc())
  470. .all()
  471. )
  472. all_tasks: List[Dict[str, Any]] = []
  473. for inst in instances:
  474. node_name_map: Dict[str, str] = {}
  475. if inst.process_version_id:
  476. for n in get_nodes_by_version(db, inst.process_version_id):
  477. node_name_map[n.node_key] = n.node_name or n.node_key
  478. tasks = (
  479. db.query(WfTask)
  480. .filter(and_(WfTask.instance_id == inst.id, WfTask.del_flag == 0))
  481. .order_by(WfTask.create_time.asc())
  482. .all()
  483. )
  484. user_ids = {t.assignee_user_id for t in tasks if t.assignee_user_id}
  485. user_name_map: Dict[str, str] = {}
  486. if user_ids:
  487. rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(user_ids)).all()
  488. user_name_map = {r.id: r.nick_name for r in rows}
  489. for t in tasks:
  490. all_tasks.append({
  491. "id": t.id,
  492. "instanceId": inst.id,
  493. "instanceStatus": inst.status,
  494. "nodeKey": t.node_key,
  495. "nodeName": node_name_map.get(t.node_key, t.node_key or ""),
  496. "assigneeUserId": t.assignee_user_id,
  497. "assigneeUserName": user_name_map.get(t.assignee_user_id, t.assignee_user_id or ""),
  498. "status": t.status,
  499. "commentText": t.comment_text,
  500. "createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
  501. "updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
  502. })
  503. return all_tasks
  504. # ---------------------------------------------------------------------------
  505. # 流程任务 wf_task
  506. # ---------------------------------------------------------------------------
  507. def create_task(
  508. db: Session,
  509. *,
  510. instance_id: str,
  511. node_key: Optional[str] = None,
  512. assignee_user_id: Optional[str] = None,
  513. tenant_no: Optional[str] = None,
  514. user_id: Optional[str] = None,
  515. ) -> WfTask:
  516. obj = WfTask(
  517. id=generate_uuid(),
  518. instance_id=instance_id,
  519. node_key=node_key,
  520. assignee_user_id=assignee_user_id,
  521. status="pending",
  522. tenant_no=tenant_no or "shanxia2.0",
  523. order_num=0,
  524. remark=None,
  525. create_by=user_id,
  526. create_dept_id="",
  527. create_time=datetime.now(),
  528. update_by=user_id,
  529. update_time=datetime.now(),
  530. del_flag=0,
  531. )
  532. db.add(obj)
  533. db.commit()
  534. db.refresh(obj)
  535. return obj
  536. def get_task(db: Session, task_id: str) -> Optional[WfTask]:
  537. return (
  538. db.query(WfTask)
  539. .filter(and_(WfTask.id == task_id, WfTask.del_flag == 0))
  540. .first()
  541. )
  542. def get_pending_tasks_by_node(db: Session, instance_id: str, node_key: str) -> List[WfTask]:
  543. return (
  544. db.query(WfTask)
  545. .filter(
  546. and_(
  547. WfTask.instance_id == instance_id,
  548. WfTask.node_key == node_key,
  549. WfTask.status == "pending",
  550. WfTask.del_flag == 0,
  551. )
  552. )
  553. .all()
  554. )
  555. def _task_row_to_dict(
  556. t: WfTask,
  557. process_name: Optional[str] = None,
  558. business_type: Optional[str] = None,
  559. business_id: Optional[str] = None,
  560. starter_user_id: Optional[str] = None,
  561. instance_status: Optional[str] = None,
  562. instance_create_time: Optional[str] = None,
  563. ) -> Dict[str, Any]:
  564. return {
  565. "id": t.id,
  566. "instanceId": t.instance_id,
  567. "nodeKey": t.node_key,
  568. "assigneeUserId": t.assignee_user_id,
  569. "status": t.status,
  570. "commentText": t.comment_text,
  571. "createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
  572. "updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
  573. "processName": process_name,
  574. "businessType": business_type,
  575. "businessId": business_id,
  576. "starterUserId": starter_user_id,
  577. "instanceStatus": instance_status,
  578. "instanceCreateTime": instance_create_time,
  579. }
  580. def list_pending_tasks(
  581. db: Session,
  582. user_id: str,
  583. *,
  584. page: int = 1,
  585. page_size: int = 10,
  586. keyword: Optional[str] = None,
  587. tenant_no: Optional[str] = None,
  588. user_info=None,
  589. ) -> Tuple[List[Dict[str, Any]], int]:
  590. q0 = (
  591. db.query(WfTask, WfProcessInstance, WfProcessVersion, WfProcessDef)
  592. .join(WfProcessInstance, WfTask.instance_id == WfProcessInstance.id)
  593. .join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
  594. .join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
  595. .filter(
  596. and_(
  597. WfTask.assignee_user_id == user_id,
  598. WfTask.status == "pending",
  599. WfTask.del_flag == 0,
  600. WfProcessInstance.del_flag == 0,
  601. WfProcessVersion.del_flag == 0,
  602. WfProcessDef.del_flag == 0,
  603. )
  604. )
  605. )
  606. if tenant_no:
  607. q0 = q0.filter(WfTask.tenant_no == tenant_no)
  608. kw = (keyword or "").strip()
  609. if kw:
  610. like = f"%{kw}%"
  611. q0 = q0.filter(
  612. or_(WfProcessDef.process_name.like(like), WfProcessDef.process_code.like(like))
  613. )
  614. if user_info:
  615. from app.core.data_scope import apply_data_scope
  616. q0 = apply_data_scope(q0, WfTask, user_info, db, tenant_no)
  617. total = q0.order_by(None).with_entities(func.count(WfTask.id)).scalar() or 0
  618. page = max(1, page)
  619. page_size = min(max(1, page_size), 100)
  620. rows = (
  621. q0.order_by(WfTask.create_time.desc())
  622. .offset((page - 1) * page_size)
  623. .limit(page_size)
  624. .all()
  625. )
  626. result = []
  627. for task, inst, ver, defn in rows:
  628. result.append(
  629. _task_row_to_dict(
  630. task,
  631. process_name=defn.process_name,
  632. business_type=inst.business_type,
  633. business_id=inst.business_id,
  634. starter_user_id=inst.starter_user_id,
  635. instance_status=inst.status,
  636. instance_create_time=inst.create_time.strftime("%Y-%m-%d %H:%M:%S") if inst.create_time else "",
  637. )
  638. )
  639. return result, int(total)
  640. def list_completed_tasks(
  641. db: Session,
  642. user_id: str,
  643. *,
  644. page: int = 1,
  645. page_size: int = 10,
  646. keyword: Optional[str] = None,
  647. tenant_no: Optional[str] = None,
  648. user_info=None,
  649. ) -> Tuple[List[Dict[str, Any]], int]:
  650. q0 = (
  651. db.query(WfTask, WfProcessInstance, WfProcessVersion, WfProcessDef)
  652. .join(WfProcessInstance, WfTask.instance_id == WfProcessInstance.id)
  653. .join(WfProcessVersion, WfProcessInstance.process_version_id == WfProcessVersion.id)
  654. .join(WfProcessDef, WfProcessVersion.process_def_id == WfProcessDef.id)
  655. .filter(
  656. and_(
  657. WfTask.assignee_user_id == user_id,
  658. WfTask.status.in_(["approved", "rejected"]),
  659. WfTask.del_flag == 0,
  660. WfProcessInstance.del_flag == 0,
  661. WfProcessVersion.del_flag == 0,
  662. WfProcessDef.del_flag == 0,
  663. )
  664. )
  665. )
  666. if tenant_no:
  667. q0 = q0.filter(WfTask.tenant_no == tenant_no)
  668. kw = (keyword or "").strip()
  669. if kw:
  670. like = f"%{kw}%"
  671. q0 = q0.filter(
  672. or_(WfProcessDef.process_name.like(like), WfProcessDef.process_code.like(like))
  673. )
  674. if user_info:
  675. from app.core.data_scope import apply_data_scope
  676. q0 = apply_data_scope(q0, WfTask, user_info, db, tenant_no)
  677. total = q0.order_by(None).with_entities(func.count(WfTask.id)).scalar() or 0
  678. page = max(1, page)
  679. page_size = min(max(1, page_size), 100)
  680. rows = (
  681. q0.order_by(WfTask.update_time.desc())
  682. .offset((page - 1) * page_size)
  683. .limit(page_size)
  684. .all()
  685. )
  686. result = []
  687. for task, inst, ver, defn in rows:
  688. result.append(
  689. _task_row_to_dict(
  690. task,
  691. process_name=defn.process_name,
  692. business_type=inst.business_type,
  693. business_id=inst.business_id,
  694. starter_user_id=inst.starter_user_id,
  695. instance_status=inst.status,
  696. instance_create_time=inst.create_time.strftime("%Y-%m-%d %H:%M:%S") if inst.create_time else "",
  697. )
  698. )
  699. return result, int(total)
  700. def total_pages(total: int, page_size: int) -> int:
  701. if total <= 0:
  702. return 0
  703. return int(math.ceil(total / float(page_size)))