workflow.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. """工作流管理接口"""
  2. from datetime import datetime
  3. from typing import Any, Dict, List, Optional
  4. from fastapi import APIRouter, Depends, Query
  5. from pydantic import AliasChoices, BaseModel, ConfigDict, Field
  6. from sqlalchemy.orm import Session
  7. from app.core.data_scope import assert_row_in_data_scope
  8. from app.core.database import get_db
  9. from app.core.menu_guard import require_admin_or_super_admin
  10. from app.core.response import success_response, error_response
  11. from app.crud import workflow as wf_crud
  12. from app.models.models import SysUser, WfNode, WfProcessDef, WfProcessInstance, WfProcessVersion, WfTask
  13. from app.services.workflow_engine import WorkflowEngine
  14. from app.utils.excel_export import (
  15. EXPORT_MAX_ROWS,
  16. ExportColumn,
  17. ExportValidationError,
  18. export_rows_to_xlsx_response,
  19. filter_export_columns,
  20. )
  21. router = APIRouter(prefix="/web/admin/workflow", tags=["工作流管理"])
  22. # ---------------------------------------------------------------------------
  23. # 业务类型 → 中文名称映射
  24. # ---------------------------------------------------------------------------
  25. _BUSINESS_TYPE_LABELS: Dict[str, str] = {
  26. "leave": "请假申请",
  27. }
  28. def _business_type_label(code: Optional[str]) -> str:
  29. if not code:
  30. return ""
  31. return _BUSINESS_TYPE_LABELS.get(code, code)
  32. def _fmt_latest_version(v: Any) -> str:
  33. if v is None or v == "":
  34. return "--"
  35. return f"V{v}"
  36. def _portal_user_info(user: dict) -> dict:
  37. dept_id = (user.get("region_id") or user.get("dept_id") or "").strip()
  38. return {
  39. "user_id": (user.get("user_id") or user.get("id") or "").strip(),
  40. "role": (user.get("role") or "").strip(),
  41. "tenant_no": (user.get("tenant_no") or "").strip(),
  42. "dept_id": dept_id,
  43. "region_id": dept_id,
  44. }
  45. def _assert_wf_instance_in_scope(
  46. db: Session,
  47. instance: WfProcessInstance,
  48. user: dict,
  49. *,
  50. skip_scope: bool = False,
  51. ) -> None:
  52. if skip_scope:
  53. return
  54. tenant_no = user.get("tenant_no", "shanxia2.0")
  55. assert_row_in_data_scope(db, instance, WfProcessInstance, _portal_user_info(user), tenant_no)
  56. # ---------------------------------------------------------------------------
  57. # 业务表单字段提供者注册表
  58. # 新增业务类型时,只需添加一个 provider 函数并注册即可,前端无需改动
  59. # ---------------------------------------------------------------------------
  60. import logging
  61. _form_field_logger = logging.getLogger("workflow.form_fields")
  62. def _leave_form_fields(db: Session, business_id: str) -> List[Dict[str, str]]:
  63. from app.crud import leave as leave_crud
  64. item = leave_crud.get_leave(db, business_id)
  65. if not item:
  66. return []
  67. leave_type_map = {"personal": "事假", "sick": "病假", "annual": "年假", "other": "其他"}
  68. fields = [
  69. {"label": "请假类型", "value": leave_type_map.get(item.get("leaveType", ""), item.get("leaveType", ""))},
  70. {"label": "请假天数", "value": f"{item.get('days', 0)} 天" if item.get("days") else "--"},
  71. {"label": "开始日期", "value": item.get("startDate", "") or "--"},
  72. {"label": "结束日期", "value": item.get("endDate", "") or "--"},
  73. ]
  74. if item.get("reason"):
  75. fields.append({"label": "请假原因", "value": item["reason"]})
  76. if item.get("remark"):
  77. fields.append({"label": "备注", "value": item["remark"]})
  78. return fields
  79. _BUSINESS_FORM_FIELD_PROVIDERS: Dict[str, Any] = {
  80. "leave": _leave_form_fields,
  81. }
  82. def _get_business_form_fields(db: Session, business_type: Optional[str], business_id: Optional[str]) -> List[Dict[str, str]]:
  83. if not business_type or not business_id:
  84. return []
  85. provider = _BUSINESS_FORM_FIELD_PROVIDERS.get(business_type)
  86. if not provider:
  87. return []
  88. try:
  89. return provider(db, business_id)
  90. except Exception as e:
  91. _form_field_logger.warning("加载业务表单字段失败: type=%s id=%s err=%s", business_type, business_id, e)
  92. return []
  93. def _enrich_task_list(db: Session, items: List[Dict[str, Any]]) -> None:
  94. starter_ids = {it.get("starterUserId") for it in items if it.get("starterUserId")}
  95. user_name_map: Dict[str, str] = {}
  96. if starter_ids:
  97. rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(starter_ids)).all()
  98. user_name_map = {r.id: r.nick_name for r in rows}
  99. for it in items:
  100. sid = it.get("starterUserId")
  101. it["starterUserName"] = user_name_map.get(sid, sid or "")
  102. it["businessTypeLabel"] = _business_type_label(it.get("businessType"))
  103. # ---------------------------------------------------------------------------
  104. # Pydantic request bodies
  105. # ---------------------------------------------------------------------------
  106. class ProcessDefCreateBody(BaseModel):
  107. model_config = ConfigDict(populate_by_name=True)
  108. processCode: str = Field(..., alias="processCode")
  109. processName: str = Field(..., alias="processName")
  110. status: Optional[str] = Field("1", alias="status")
  111. orderNum: Optional[int] = Field(0, alias="orderNum")
  112. remark: Optional[str] = None
  113. class ProcessDefUpdateBody(BaseModel):
  114. model_config = ConfigDict(populate_by_name=True)
  115. processCode: Optional[str] = Field(None, alias="processCode")
  116. processName: Optional[str] = Field(None, alias="processName")
  117. status: Optional[str] = Field(None, alias="status")
  118. orderNum: Optional[int] = Field(None, alias="orderNum")
  119. remark: Optional[str] = None
  120. class PublishVersionBody(BaseModel):
  121. model_config = ConfigDict(populate_by_name=True)
  122. graphJson: Optional[str] = Field(None, alias="graphJson")
  123. nodes: List[Dict[str, Any]] = Field(default_factory=list, alias="nodes")
  124. transitions: List[Dict[str, Any]] = Field(default_factory=list, alias="transitions")
  125. remark: Optional[str] = None
  126. class StartInstanceBody(BaseModel):
  127. model_config = ConfigDict(populate_by_name=True)
  128. processCode: str = Field(..., alias="processCode")
  129. businessType: Optional[str] = Field(None, alias="businessType")
  130. businessId: Optional[str] = Field(None, alias="businessId")
  131. variables: Optional[Dict[str, Any]] = None
  132. callbackUrl: Optional[str] = Field(None, alias="callbackUrl")
  133. class TaskActionBody(BaseModel):
  134. """审批意见:前端传 commentText,兼容 comment。"""
  135. model_config = ConfigDict(populate_by_name=True)
  136. comment: Optional[str] = Field(
  137. default=None,
  138. validation_alias=AliasChoices("comment", "commentText"),
  139. )
  140. # ---------------------------------------------------------------------------
  141. # 流程定义 CRUD
  142. # ---------------------------------------------------------------------------
  143. @router.get("/defs")
  144. def list_process_defs(
  145. page: int = Query(1, ge=1),
  146. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  147. keyword: Optional[str] = None,
  148. status: Optional[str] = None,
  149. _user: dict = Depends(require_admin_or_super_admin),
  150. db: Session = Depends(get_db),
  151. ):
  152. tenant_no = _user.get("tenant_no", "shanxia2.0")
  153. items, total = wf_crud.list_process_defs(
  154. db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no, user_info=_user
  155. )
  156. tp = wf_crud.total_pages(total, pageSize)
  157. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  158. PROCESS_DEF_EXPORT_COLUMNS = [
  159. ExportColumn(title="流程编码", field="processCode"),
  160. ExportColumn(title="流程名称", field="processName"),
  161. ExportColumn(title="状态", field="status", dict_type="sys_status"),
  162. ExportColumn(title="最新版本", field="latestVersion", formatter=_fmt_latest_version),
  163. ExportColumn(title="创建时间", field="createTime"),
  164. ]
  165. class ProcessDefExportBody(BaseModel):
  166. keyword: Optional[str] = None
  167. status: Optional[str] = None
  168. ids: Optional[List[str]] = None
  169. columns: Optional[List[str]] = None
  170. @router.post("/defs/export")
  171. def export_process_defs(
  172. body: ProcessDefExportBody,
  173. user: dict = Depends(require_admin_or_super_admin),
  174. db: Session = Depends(get_db),
  175. ):
  176. """导出流程定义列表(当前筛选全量,或 ids 已选)。"""
  177. try:
  178. tenant_no = user.get("tenant_no", "shanxia2.0")
  179. items, _ = wf_crud.list_process_defs(
  180. db,
  181. page=1,
  182. page_size=EXPORT_MAX_ROWS,
  183. keyword=body.keyword,
  184. status=body.status,
  185. tenant_no=tenant_no,
  186. user_info=user,
  187. )
  188. if body.ids:
  189. idset = set(body.ids)
  190. items = [r for r in items if r.get("id") in idset]
  191. cols = filter_export_columns(PROCESS_DEF_EXPORT_COLUMNS, body.columns)
  192. date_str = datetime.now().strftime("%Y%m%d")
  193. return export_rows_to_xlsx_response(
  194. db,
  195. tenant_no=tenant_no,
  196. sheet_title="流程定义",
  197. columns=cols,
  198. rows=items,
  199. filename_stem=f"流程定义_{date_str}",
  200. )
  201. except ExportValidationError as e:
  202. return error_response(400, str(e))
  203. except Exception as e:
  204. return error_response(500, str(e))
  205. @router.post("/defs")
  206. def create_process_def(
  207. body: ProcessDefCreateBody,
  208. user: dict = Depends(require_admin_or_super_admin),
  209. db: Session = Depends(get_db),
  210. ):
  211. try:
  212. tenant_no = user.get("tenant_no", "shanxia2.0")
  213. obj = wf_crud.create_process_def(
  214. db,
  215. process_code=body.processCode,
  216. process_name=body.processName,
  217. tenant_no=tenant_no,
  218. status=body.status,
  219. order_num=body.orderNum,
  220. remark=body.remark,
  221. user_id=user.get("user_id"),
  222. )
  223. return success_response(wf_crud._row_to_dict(obj), message="创建成功")
  224. except ValueError as e:
  225. return error_response(400, str(e))
  226. @router.put("/defs/{def_id}")
  227. def update_process_def(
  228. def_id: str,
  229. body: ProcessDefUpdateBody,
  230. user: dict = Depends(require_admin_or_super_admin),
  231. db: Session = Depends(get_db),
  232. ):
  233. try:
  234. payload: Dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=True)
  235. obj = wf_crud.update_process_def(
  236. db,
  237. def_id,
  238. process_code=payload.get("processCode"),
  239. process_name=payload.get("processName"),
  240. status=payload.get("status"),
  241. order_num=payload.get("orderNum"),
  242. remark=payload.get("remark"),
  243. user_id=user.get("user_id"),
  244. tenant_no=user.get("tenant_no", "shanxia2.0"),
  245. )
  246. return success_response(wf_crud._row_to_dict(obj), message="更新成功")
  247. except ValueError as e:
  248. return error_response(400, str(e))
  249. @router.delete("/defs/{def_id}")
  250. def delete_process_def(
  251. def_id: str,
  252. _user: dict = Depends(require_admin_or_super_admin),
  253. db: Session = Depends(get_db),
  254. ):
  255. try:
  256. wf_crud.soft_delete_process_def(db, def_id, user_info=_user)
  257. return success_response(None, message="删除成功")
  258. except ValueError as e:
  259. return error_response(400, str(e))
  260. # ---------------------------------------------------------------------------
  261. # 版本发布 / 查看
  262. # ---------------------------------------------------------------------------
  263. @router.post("/defs/{def_id}/publish")
  264. def publish_version(
  265. def_id: str,
  266. body: PublishVersionBody,
  267. user: dict = Depends(require_admin_or_super_admin),
  268. db: Session = Depends(get_db),
  269. ):
  270. try:
  271. tenant_no = user.get("tenant_no", "shanxia2.0")
  272. ver = wf_crud.publish_version(
  273. db,
  274. process_def_id=def_id,
  275. graph_json=body.graphJson or "",
  276. nodes=body.nodes,
  277. transitions=body.transitions,
  278. tenant_no=tenant_no,
  279. remark=body.remark,
  280. user_id=user.get("user_id"),
  281. )
  282. return success_response({"versionId": ver.id, "versionNo": ver.version_no}, message="发布成功")
  283. except ValueError as e:
  284. return error_response(400, str(e))
  285. @router.get("/defs/{def_id}/versions")
  286. def list_versions(
  287. def_id: str,
  288. _user: dict = Depends(require_admin_or_super_admin),
  289. db: Session = Depends(get_db),
  290. ):
  291. tenant_no = _user.get("tenant_no", "shanxia2.0")
  292. items = wf_crud.list_versions(db, def_id, tenant_no=tenant_no, user_info=_user)
  293. return success_response(items)
  294. @router.get("/defs/{def_id}/designer")
  295. def get_designer_data(
  296. def_id: str,
  297. _user: dict = Depends(require_admin_or_super_admin),
  298. db: Session = Depends(get_db),
  299. ):
  300. ver = wf_crud.get_latest_version(db, def_id)
  301. if not ver:
  302. return success_response({"graphJson": None, "nodes": [], "transitions": []})
  303. nodes = wf_crud.get_nodes_by_version(db, ver.id)
  304. transitions = wf_crud.get_transitions_by_version(db, ver.id)
  305. return success_response({
  306. "graphJson": ver.graph_json,
  307. "versionId": ver.id,
  308. "versionNo": ver.version_no,
  309. "nodes": [
  310. {"nodeKey": n.node_key, "nodeType": n.node_type, "nodeName": n.node_name, "configJson": n.config_json}
  311. for n in nodes
  312. ],
  313. "transitions": [
  314. {"fromNodeKey": t.from_node_key, "toNodeKey": t.to_node_key, "conditionExpr": t.condition_expr}
  315. for t in transitions
  316. ],
  317. })
  318. # ---------------------------------------------------------------------------
  319. # 流程实例
  320. # ---------------------------------------------------------------------------
  321. @router.post("/instances")
  322. def start_instance(
  323. body: StartInstanceBody,
  324. user: dict = Depends(require_admin_or_super_admin),
  325. db: Session = Depends(get_db),
  326. ):
  327. try:
  328. tenant_no = user.get("tenant_no", "shanxia2.0")
  329. user_id = user.get("user_id") or user.get("id")
  330. defn = (
  331. db.query(WfProcessDef)
  332. .filter(
  333. WfProcessDef.process_code == body.processCode,
  334. WfProcessDef.del_flag == 0,
  335. )
  336. .first()
  337. )
  338. if not defn:
  339. return error_response(404, "流程定义不存在")
  340. engine = WorkflowEngine(db)
  341. instance = engine.start_process(
  342. process_def_id=defn.id,
  343. business_type=body.businessType,
  344. business_id=body.businessId,
  345. starter_user_id=user_id,
  346. variables=body.variables,
  347. callback_url=body.callbackUrl,
  348. tenant_no=tenant_no,
  349. user_id=user_id,
  350. )
  351. return success_response({
  352. "instanceId": instance.id,
  353. "status": instance.status,
  354. }, message="发起成功")
  355. except ValueError as e:
  356. return error_response(400, str(e))
  357. @router.get("/instances/my")
  358. def list_my_instances(
  359. page: int = Query(1, ge=1),
  360. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  361. keyword: Optional[str] = None,
  362. status: Optional[str] = None,
  363. user: dict = Depends(require_admin_or_super_admin),
  364. db: Session = Depends(get_db),
  365. ):
  366. user_id = user.get("user_id") or user.get("id")
  367. tenant_no = user.get("tenant_no", "shanxia2.0")
  368. items, total = wf_crud.list_my_instances(
  369. db, user_id, page=page, page_size=pageSize,
  370. keyword=keyword, status=status, tenant_no=tenant_no, user_info=user,
  371. )
  372. _enrich_task_list(db, items)
  373. tp = wf_crud.total_pages(total, pageSize)
  374. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  375. MY_INSTANCE_EXPORT_COLUMNS = [
  376. ExportColumn(title="流程名称", field="processName"),
  377. ExportColumn(title="业务类型", field="businessTypeLabel"),
  378. ExportColumn(title="业务单号", field="businessId"),
  379. ExportColumn(title="状态", field="status", dict_type="wf_process_status"),
  380. ExportColumn(title="发起时间", field="createTime"),
  381. ]
  382. class MyInstanceExportBody(BaseModel):
  383. keyword: Optional[str] = None
  384. status: Optional[str] = None
  385. ids: Optional[List[str]] = None
  386. columns: Optional[List[str]] = None
  387. @router.post("/instances/my/export")
  388. def export_my_instances(
  389. body: MyInstanceExportBody,
  390. user: dict = Depends(require_admin_or_super_admin),
  391. db: Session = Depends(get_db),
  392. ):
  393. """导出我的申请列表(当前筛选全量,或 ids 已选)。"""
  394. try:
  395. user_id = user.get("user_id") or user.get("id")
  396. tenant_no = user.get("tenant_no", "shanxia2.0")
  397. items, _ = wf_crud.list_my_instances(
  398. db,
  399. user_id,
  400. page=1,
  401. page_size=EXPORT_MAX_ROWS,
  402. keyword=body.keyword,
  403. status=body.status,
  404. tenant_no=tenant_no,
  405. user_info=user,
  406. )
  407. _enrich_task_list(db, items)
  408. if body.ids:
  409. idset = set(body.ids)
  410. items = [r for r in items if r.get("id") in idset]
  411. cols = filter_export_columns(MY_INSTANCE_EXPORT_COLUMNS, body.columns)
  412. date_str = datetime.now().strftime("%Y%m%d")
  413. return export_rows_to_xlsx_response(
  414. db,
  415. tenant_no=tenant_no,
  416. sheet_title="我的申请",
  417. columns=cols,
  418. rows=items,
  419. filename_stem=f"我的申请_{date_str}",
  420. )
  421. except ExportValidationError as e:
  422. return error_response(400, str(e))
  423. except Exception as e:
  424. return error_response(500, str(e))
  425. @router.get("/instances/{instance_id}")
  426. def get_instance_detail(
  427. instance_id: str,
  428. user: dict = Depends(require_admin_or_super_admin),
  429. db: Session = Depends(get_db),
  430. ):
  431. instance = wf_crud.get_instance(db, instance_id)
  432. if not instance:
  433. return error_response(404, "流程实例不存在")
  434. _assert_wf_instance_in_scope(db, instance, user)
  435. tasks = db.query(WfTask).filter(
  436. WfTask.instance_id == instance_id,
  437. WfTask.del_flag == 0,
  438. ).order_by(WfTask.create_time.asc()).all()
  439. # 查询节点名称映射 node_key → node_name
  440. node_name_map: Dict[str, str] = {}
  441. if instance.process_version_id:
  442. for n in wf_crud.get_nodes_by_version(db, instance.process_version_id):
  443. node_name_map[n.node_key] = n.node_name or n.node_key
  444. # 批量查询用户名
  445. user_ids = {t.assignee_user_id for t in tasks if t.assignee_user_id}
  446. if instance.starter_user_id:
  447. user_ids.add(instance.starter_user_id)
  448. user_name_map: Dict[str, str] = {}
  449. if user_ids:
  450. rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(user_ids)).all()
  451. user_name_map = {r.id: r.nick_name for r in rows}
  452. return success_response({
  453. "id": instance.id,
  454. "businessType": instance.business_type,
  455. "businessTypeLabel": _business_type_label(instance.business_type),
  456. "businessId": instance.business_id,
  457. "businessFormFields": _get_business_form_fields(db, instance.business_type, instance.business_id),
  458. "starterUserId": instance.starter_user_id,
  459. "starterUserName": user_name_map.get(instance.starter_user_id, instance.starter_user_id or ""),
  460. "status": instance.status,
  461. "variablesJson": instance.variables_json,
  462. "callbackUrl": instance.callback_url,
  463. "createTime": instance.create_time.strftime("%Y-%m-%d %H:%M:%S") if instance.create_time else "",
  464. "tasks": [
  465. {
  466. "id": t.id,
  467. "nodeKey": t.node_key,
  468. "nodeName": node_name_map.get(t.node_key, t.node_key or ""),
  469. "assigneeUserId": t.assignee_user_id,
  470. "assigneeUserName": user_name_map.get(t.assignee_user_id, t.assignee_user_id or ""),
  471. "status": t.status,
  472. "commentText": t.comment_text,
  473. "createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
  474. "updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
  475. }
  476. for t in tasks
  477. ],
  478. })
  479. # ---------------------------------------------------------------------------
  480. # 待办 / 已办
  481. # ---------------------------------------------------------------------------
  482. @router.get("/tasks/pending")
  483. def list_pending_tasks(
  484. page: int = Query(1, ge=1),
  485. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  486. keyword: Optional[str] = None,
  487. user: dict = Depends(require_admin_or_super_admin),
  488. db: Session = Depends(get_db),
  489. ):
  490. user_id = user.get("user_id") or user.get("id")
  491. tenant_no = user.get("tenant_no", "shanxia2.0")
  492. items, total = wf_crud.list_pending_tasks(
  493. db, user_id, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
  494. )
  495. _enrich_task_list(db, items)
  496. tp = wf_crud.total_pages(total, pageSize)
  497. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  498. PENDING_TASK_EXPORT_COLUMNS = [
  499. ExportColumn(title="流程名称", field="processName"),
  500. ExportColumn(title="业务类型", field="businessTypeLabel"),
  501. ExportColumn(title="业务单号", field="businessId"),
  502. ExportColumn(title="发起人", field="starterUserName"),
  503. ExportColumn(title="到达时间", field="createTime"),
  504. ]
  505. class PendingTaskExportBody(BaseModel):
  506. keyword: Optional[str] = None
  507. ids: Optional[List[str]] = None
  508. columns: Optional[List[str]] = None
  509. @router.post("/tasks/pending/export")
  510. def export_pending_tasks(
  511. body: PendingTaskExportBody,
  512. user: dict = Depends(require_admin_or_super_admin),
  513. db: Session = Depends(get_db),
  514. ):
  515. """导出待办任务列表(当前筛选全量,或 ids 已选)。"""
  516. try:
  517. user_id = user.get("user_id") or user.get("id")
  518. tenant_no = user.get("tenant_no", "shanxia2.0")
  519. items, _ = wf_crud.list_pending_tasks(
  520. db,
  521. user_id,
  522. page=1,
  523. page_size=EXPORT_MAX_ROWS,
  524. keyword=body.keyword,
  525. tenant_no=tenant_no,
  526. user_info=user,
  527. )
  528. _enrich_task_list(db, items)
  529. if body.ids:
  530. idset = set(body.ids)
  531. items = [r for r in items if r.get("id") in idset]
  532. cols = filter_export_columns(PENDING_TASK_EXPORT_COLUMNS, body.columns)
  533. date_str = datetime.now().strftime("%Y%m%d")
  534. return export_rows_to_xlsx_response(
  535. db,
  536. tenant_no=tenant_no,
  537. sheet_title="待办任务",
  538. columns=cols,
  539. rows=items,
  540. filename_stem=f"待办任务_{date_str}",
  541. )
  542. except ExportValidationError as e:
  543. return error_response(400, str(e))
  544. except Exception as e:
  545. return error_response(500, str(e))
  546. @router.get("/tasks/completed")
  547. def list_completed_tasks(
  548. page: int = Query(1, ge=1),
  549. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  550. keyword: Optional[str] = None,
  551. user: dict = Depends(require_admin_or_super_admin),
  552. db: Session = Depends(get_db),
  553. ):
  554. user_id = user.get("user_id") or user.get("id")
  555. tenant_no = user.get("tenant_no", "shanxia2.0")
  556. items, total = wf_crud.list_completed_tasks(
  557. db, user_id, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
  558. )
  559. _enrich_task_list(db, items)
  560. tp = wf_crud.total_pages(total, pageSize)
  561. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  562. COMPLETED_TASK_EXPORT_COLUMNS = [
  563. ExportColumn(title="流程名称", field="processName"),
  564. ExportColumn(title="业务类型", field="businessTypeLabel"),
  565. ExportColumn(title="业务单号", field="businessId"),
  566. ExportColumn(title="发起人", field="starterUserName"),
  567. ExportColumn(title="审批结果", field="status", dict_type="wf_process_status"),
  568. ExportColumn(title="审批意见", field="commentText"),
  569. ExportColumn(title="审批时间", field="updateTime"),
  570. ]
  571. class CompletedTaskExportBody(BaseModel):
  572. keyword: Optional[str] = None
  573. ids: Optional[List[str]] = None
  574. columns: Optional[List[str]] = None
  575. @router.post("/tasks/completed/export")
  576. def export_completed_tasks(
  577. body: CompletedTaskExportBody,
  578. user: dict = Depends(require_admin_or_super_admin),
  579. db: Session = Depends(get_db),
  580. ):
  581. """导出已办任务列表(当前筛选全量,或 ids 已选)。"""
  582. try:
  583. user_id = user.get("user_id") or user.get("id")
  584. tenant_no = user.get("tenant_no", "shanxia2.0")
  585. items, _ = wf_crud.list_completed_tasks(
  586. db,
  587. user_id,
  588. page=1,
  589. page_size=EXPORT_MAX_ROWS,
  590. keyword=body.keyword,
  591. tenant_no=tenant_no,
  592. user_info=user,
  593. )
  594. _enrich_task_list(db, items)
  595. if body.ids:
  596. idset = set(body.ids)
  597. items = [r for r in items if r.get("id") in idset]
  598. cols = filter_export_columns(COMPLETED_TASK_EXPORT_COLUMNS, body.columns)
  599. date_str = datetime.now().strftime("%Y%m%d")
  600. return export_rows_to_xlsx_response(
  601. db,
  602. tenant_no=tenant_no,
  603. sheet_title="已办任务",
  604. columns=cols,
  605. rows=items,
  606. filename_stem=f"已办任务_{date_str}",
  607. )
  608. except ExportValidationError as e:
  609. return error_response(400, str(e))
  610. except Exception as e:
  611. return error_response(500, str(e))
  612. # ---------------------------------------------------------------------------
  613. # 审批 / 驳回
  614. # ---------------------------------------------------------------------------
  615. @router.post("/tasks/{task_id}/approve")
  616. def approve_task(
  617. task_id: str,
  618. body: TaskActionBody,
  619. user: dict = Depends(require_admin_or_super_admin),
  620. db: Session = Depends(get_db),
  621. ):
  622. try:
  623. task = wf_crud.get_task(db, task_id)
  624. if not task:
  625. return error_response(404, "任务不存在")
  626. instance = wf_crud.get_instance(db, task.instance_id)
  627. if not instance:
  628. return error_response(404, "流程实例不存在")
  629. user_id = (user.get("user_id") or user.get("id") or "").strip()
  630. skip_scope = task.assignee_user_id == user_id
  631. _assert_wf_instance_in_scope(db, instance, user, skip_scope=skip_scope)
  632. tenant_no = user.get("tenant_no", "shanxia2.0")
  633. engine = WorkflowEngine(db)
  634. instance = engine.approve_task(task_id, body.comment or "", tenant_no)
  635. return success_response({"instanceId": instance.id, "status": instance.status}, message="审批成功")
  636. except ValueError as e:
  637. return error_response(400, str(e))
  638. @router.post("/tasks/{task_id}/reject")
  639. def reject_task(
  640. task_id: str,
  641. body: TaskActionBody,
  642. user: dict = Depends(require_admin_or_super_admin),
  643. db: Session = Depends(get_db),
  644. ):
  645. try:
  646. task = wf_crud.get_task(db, task_id)
  647. if not task:
  648. return error_response(404, "任务不存在")
  649. instance = wf_crud.get_instance(db, task.instance_id)
  650. if not instance:
  651. return error_response(404, "流程实例不存在")
  652. user_id = (user.get("user_id") or user.get("id") or "").strip()
  653. skip_scope = task.assignee_user_id == user_id
  654. _assert_wf_instance_in_scope(db, instance, user, skip_scope=skip_scope)
  655. tenant_no = user.get("tenant_no", "shanxia2.0")
  656. engine = WorkflowEngine(db)
  657. instance = engine.reject_task(task_id, body.comment or "", tenant_no)
  658. return success_response({"instanceId": instance.id, "status": instance.status}, message="已驳回")
  659. except ValueError as e:
  660. return error_response(400, str(e))
  661. # ---------------------------------------------------------------------------
  662. # 按业务查询所有审批历史(跨实例)
  663. # ---------------------------------------------------------------------------
  664. @router.get("/instances/by-business/{business_type}/{business_id}")
  665. def get_business_all_tasks(
  666. business_type: str,
  667. business_id: str,
  668. user: dict = Depends(require_admin_or_super_admin),
  669. db: Session = Depends(get_db),
  670. ):
  671. instances = (
  672. db.query(WfProcessInstance)
  673. .filter(
  674. WfProcessInstance.business_type == business_type,
  675. WfProcessInstance.business_id == business_id,
  676. WfProcessInstance.del_flag == 0,
  677. )
  678. .order_by(WfProcessInstance.create_time.asc())
  679. .all()
  680. )
  681. if instances:
  682. _assert_wf_instance_in_scope(db, instances[0], user)
  683. tasks = wf_crud.get_all_tasks_for_business(db, business_type, business_id)
  684. business_form_fields = _get_business_form_fields(db, business_type, business_id)
  685. return success_response({
  686. "businessType": business_type,
  687. "businessId": business_id,
  688. "businessFormFields": business_form_fields,
  689. "tasks": tasks,
  690. })