workflow_callback.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """工作流回调服务 —— 流程状态变更时通知业务系统"""
  2. from __future__ import annotations
  3. import logging
  4. from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
  5. import requests
  6. if TYPE_CHECKING:
  7. from app.models.models import WfProcessInstance
  8. logger = logging.getLogger("sqxp.workflow_callback")
  9. # ---------------------------------------------------------------------------
  10. # 本地回调注册表:内部路径(以 / 开头)直接在进程内调用,不走 HTTP
  11. # ---------------------------------------------------------------------------
  12. _LocalCallbackFn = Callable[[Any, str, str], None] # (db, business_id, event) -> None
  13. _LOCAL_CALLBACKS: Dict[str, _LocalCallbackFn] = {}
  14. def register_local_callback(path: str, fn: _LocalCallbackFn) -> None:
  15. """注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。"""
  16. _LOCAL_CALLBACKS[path] = fn
  17. def _dispatch_local(db: Any, instance: WfProcessInstance, event: str) -> bool:
  18. """尝试本地直调。返回 True 表示已处理,False 表示无匹配的本地回调。"""
  19. url = instance.callback_url or ""
  20. if not url.startswith("/"):
  21. return False
  22. for prefix, fn in _LOCAL_CALLBACKS.items():
  23. if url.startswith(prefix):
  24. try:
  25. fn(db, instance.business_id or "", event)
  26. logger.info(
  27. "工作流本地回调成功: event=%s instance=%s path=%s",
  28. event,
  29. instance.id,
  30. url,
  31. )
  32. return True
  33. except Exception:
  34. logger.exception(
  35. "工作流本地回调失败: event=%s instance=%s path=%s",
  36. event,
  37. instance.id,
  38. url,
  39. )
  40. return True # 已匹配但执行失败,不再走 HTTP
  41. return False
  42. # ---------------------------------------------------------------------------
  43. # trigger_callback(对外入口)
  44. # ---------------------------------------------------------------------------
  45. def trigger_callback(db: Any, instance: WfProcessInstance, event: str) -> None:
  46. """流程实例状态变更后,向业务系统发送回调通知。
  47. 优先级:
  48. 1. 如果 callback_url 是内部路径(以 / 开头),优先尝试本地直调
  49. 2. 本地无匹配时,回退到 HTTP 回调(需完整 URL)
  50. Args:
  51. db: 数据库会话。
  52. instance: 流程实例 ORM 对象。
  53. event: 事件类型,如 approved / rejected / completed。
  54. """
  55. if not instance.callback_url:
  56. return
  57. # 尝试本地直调
  58. if _dispatch_local(db, instance, event):
  59. return
  60. # 回退到 HTTP 回调
  61. payload: Dict[str, Any] = {
  62. "event": event,
  63. "instanceId": instance.id,
  64. "businessType": instance.business_type,
  65. "businessId": instance.business_id,
  66. "status": instance.status,
  67. }
  68. try:
  69. resp = requests.post(
  70. instance.callback_url,
  71. json=payload,
  72. timeout=10,
  73. headers={"Content-Type": "application/json"},
  74. )
  75. logger.info(
  76. "工作流HTTP回调成功: event=%s instance=%s status=%d",
  77. event,
  78. instance.id,
  79. resp.status_code,
  80. )
  81. except Exception:
  82. logger.exception(
  83. "工作流HTTP回调失败: event=%s instance=%s url=%s",
  84. event,
  85. instance.id,
  86. instance.callback_url,
  87. )