workflow.py 24 KB

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