oa_wechat_notify.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. """OA 任务创建后服务号通知(commit 后异步)。"""
  2. from __future__ import annotations
  3. import logging
  4. import threading
  5. from datetime import datetime
  6. from typing import Iterable, List, Optional
  7. from urllib.parse import urlencode
  8. from app.core.config import settings
  9. from app.core.database import SessionLocal
  10. from app.models.models import OaApp, OaInstance, OaTask, SysUser
  11. from app.services.wechat_mp_message import (
  12. send_oa_applicant_progress_notify,
  13. send_oa_approval_notify,
  14. send_oa_cc_notify,
  15. )
  16. logger = logging.getLogger(__name__)
  17. # 微信模板 const4 枚举,必须原样下发
  18. RESULT_PASS = "通过"
  19. RESULT_FAIL = "失败"
  20. RESULT_TRANSFER = "转办"
  21. RESULT_OTHER = "其他"
  22. # 模板消息关闭详情后回退的小程序页(与列表入口对齐)
  23. RETURN_PENDING = "/pages/oa/pending/index"
  24. RETURN_CC = "/pages/oa/cc/index"
  25. RETURN_MINE = "/pages/oa/mine/index"
  26. _ACTION_RESULT = {
  27. "approve": RESULT_PASS,
  28. "reject": RESULT_FAIL,
  29. "transfer": RESULT_TRANSFER,
  30. "return": RESULT_OTHER,
  31. "add_sign": RESULT_OTHER,
  32. }
  33. def map_oa_action_to_result(action: str) -> str:
  34. return _ACTION_RESULT.get((action or "").strip(), RESULT_OTHER)
  35. def build_oa_detail_pagepath(
  36. *,
  37. instance_id: str,
  38. task_id: str = "",
  39. mode: str,
  40. return_path: str = "",
  41. ) -> str:
  42. """拼小程序 pagepath(无前导 /);query 做 urlencode,保证 returnPath 含斜杠可用。"""
  43. params = {
  44. "instanceId": (instance_id or "").strip(),
  45. "mode": (mode or "view").strip() or "view",
  46. }
  47. tid = (task_id or "").strip()
  48. if tid and tid != "0":
  49. params["taskId"] = tid
  50. rp = (return_path or "").strip()
  51. if rp:
  52. params["returnPath"] = rp
  53. return "pages/oa/webview/index?" + urlencode(params)
  54. def _build_matter_title(*, applicant: str, process_name: str, fallback: str) -> str:
  55. """申请事项/业务名称:申请人 - 流程名称。"""
  56. parts = [p for p in [(applicant or "").strip(), (process_name or "").strip()] if p]
  57. if parts:
  58. return " - ".join(parts)
  59. return (fallback or "").strip() or "审批"
  60. def _resolve_process_and_applicant(db, inst: Optional[OaInstance]):
  61. applicant = ""
  62. process_name = ""
  63. if not inst:
  64. return applicant, process_name
  65. if inst.initiator_user_id:
  66. ini = db.query(SysUser).filter(SysUser.id == inst.initiator_user_id).first()
  67. applicant = (ini.nick_name if ini else "") or ""
  68. if inst.app_id:
  69. app = (
  70. db.query(OaApp)
  71. .filter(OaApp.id == inst.app_id, OaApp.del_flag == 0)
  72. .first()
  73. )
  74. process_name = (app.app_name if app else "") or ""
  75. if not process_name:
  76. process_name = (inst.title or "").strip()
  77. return applicant, process_name
  78. def _send_one(db, task: OaTask) -> None:
  79. uid = (task.assignee_user_id or "").strip()
  80. if not uid:
  81. return
  82. user = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
  83. mp = (getattr(user, "wx_mp_openid", None) or "").strip() if user else ""
  84. if not mp:
  85. logger.info("oa wx notify skip: no mp_openid user=%s task=%s", uid, task.id)
  86. return
  87. inst = (
  88. db.query(OaInstance)
  89. .filter(OaInstance.id == task.instance_id, OaInstance.del_flag == 0)
  90. .first()
  91. )
  92. applicant, process_name = _resolve_process_and_applicant(db, inst)
  93. fallback = (inst.title if inst else "") or (task.node_name or "审批")
  94. title = _build_matter_title(
  95. applicant=applicant, process_name=process_name, fallback=fallback
  96. )
  97. time_text = datetime.now().strftime("%Y-%m-%d %H:%M")
  98. node_name = task.node_name or ""
  99. if task.task_type == "approval" and (task.status or "") == "pending":
  100. pagepath = build_oa_detail_pagepath(
  101. instance_id=task.instance_id,
  102. task_id=task.id,
  103. mode="approve",
  104. return_path=RETURN_PENDING,
  105. )
  106. send_oa_approval_notify(
  107. mp_openid=mp,
  108. pagepath=pagepath,
  109. title=title,
  110. applicant=applicant,
  111. node_name=node_name,
  112. time_text=time_text,
  113. process_name=process_name,
  114. )
  115. elif task.task_type == "cc":
  116. pagepath = build_oa_detail_pagepath(
  117. instance_id=task.instance_id,
  118. task_id=task.id,
  119. mode="view",
  120. return_path=RETURN_CC,
  121. )
  122. send_oa_cc_notify(
  123. mp_openid=mp,
  124. pagepath=pagepath,
  125. title=title,
  126. applicant=applicant,
  127. node_name=node_name,
  128. time_text=time_text,
  129. process_name=process_name,
  130. )
  131. def _send_applicant_progress(
  132. db,
  133. *,
  134. instance_id: str,
  135. actor_user_id: str,
  136. result: str,
  137. task_id: str = "",
  138. ) -> None:
  139. iid = (instance_id or "").strip()
  140. if not iid:
  141. return
  142. inst = (
  143. db.query(OaInstance)
  144. .filter(OaInstance.id == iid, OaInstance.del_flag == 0)
  145. .first()
  146. )
  147. if not inst or not (inst.initiator_user_id or "").strip():
  148. return
  149. ini = (
  150. db.query(SysUser)
  151. .filter(SysUser.id == inst.initiator_user_id, SysUser.del_flag == 0)
  152. .first()
  153. )
  154. mp = (getattr(ini, "wx_mp_openid", None) or "").strip() if ini else ""
  155. if not mp:
  156. logger.info(
  157. "oa wx applicant progress skip: no mp_openid initiator=%s inst=%s",
  158. inst.initiator_user_id,
  159. iid,
  160. )
  161. return
  162. applicant = (ini.nick_name if ini else "") or ""
  163. process_name = ""
  164. if inst.app_id:
  165. app = (
  166. db.query(OaApp)
  167. .filter(OaApp.id == inst.app_id, OaApp.del_flag == 0)
  168. .first()
  169. )
  170. process_name = (app.app_name if app else "") or ""
  171. if not process_name:
  172. process_name = (inst.title or "").strip()
  173. business_name = _build_matter_title(
  174. applicant=applicant,
  175. process_name=process_name,
  176. fallback=(inst.title or "审批"),
  177. )
  178. actor = None
  179. aid = (actor_user_id or "").strip()
  180. if aid:
  181. actor = (
  182. db.query(SysUser)
  183. .filter(SysUser.id == aid, SysUser.del_flag == 0)
  184. .first()
  185. )
  186. approver_name = (actor.nick_name if actor else "") or ""
  187. pagepath = build_oa_detail_pagepath(
  188. instance_id=iid,
  189. task_id="",
  190. mode="view",
  191. return_path=RETURN_MINE,
  192. )
  193. send_oa_applicant_progress_notify(
  194. mp_openid=mp,
  195. pagepath=pagepath,
  196. business_name=business_name,
  197. process_name=process_name,
  198. approver_name=approver_name,
  199. result=(result or RESULT_OTHER).strip() or RESULT_OTHER,
  200. time_text=datetime.now().strftime("%Y-%m-%d %H:%M"),
  201. )
  202. def _oa_wx_notify_enabled() -> bool:
  203. return bool(getattr(settings, "WX_MP_NOTIFY_ENABLED", False))
  204. def notify_oa_tasks_sync(task_ids: Iterable[str]) -> None:
  205. if not _oa_wx_notify_enabled():
  206. logger.info("oa wx notify skipped: WX_MP_NOTIFY_ENABLED=false")
  207. return
  208. ids = [str(x).strip() for x in task_ids if str(x).strip()]
  209. if not ids:
  210. return
  211. db = None
  212. try:
  213. db = SessionLocal()
  214. tasks = (
  215. db.query(OaTask)
  216. .filter(OaTask.id.in_(ids), OaTask.del_flag == 0)
  217. .all()
  218. )
  219. for t in tasks:
  220. try:
  221. _send_one(db, t)
  222. except Exception:
  223. logger.exception("oa wx notify failed task=%s", t.id)
  224. except Exception:
  225. logger.exception("oa wx notify batch failed ids=%s", ids)
  226. finally:
  227. if db is not None:
  228. db.close()
  229. def notify_oa_applicant_progress_sync(
  230. *,
  231. instance_id: str,
  232. actor_user_id: str,
  233. result: str,
  234. task_id: str = "",
  235. ) -> None:
  236. if not _oa_wx_notify_enabled():
  237. logger.info("oa wx applicant progress skipped: WX_MP_NOTIFY_ENABLED=false")
  238. return
  239. db = None
  240. try:
  241. db = SessionLocal()
  242. _send_applicant_progress(
  243. db,
  244. instance_id=instance_id,
  245. actor_user_id=actor_user_id,
  246. result=result,
  247. task_id=task_id,
  248. )
  249. except Exception:
  250. logger.exception(
  251. "oa wx applicant progress failed inst=%s actor=%s",
  252. instance_id,
  253. actor_user_id,
  254. )
  255. finally:
  256. if db is not None:
  257. db.close()
  258. def schedule_oa_task_notifies(task_ids: List[str]) -> None:
  259. if not _oa_wx_notify_enabled():
  260. logger.info("oa wx notify schedule skipped: WX_MP_NOTIFY_ENABLED=false")
  261. return
  262. ids = [str(x).strip() for x in (task_ids or []) if str(x).strip()]
  263. if not ids:
  264. return
  265. threading.Thread(
  266. target=notify_oa_tasks_sync, args=(ids,), daemon=True, name="oa-wx-notify"
  267. ).start()
  268. def schedule_oa_applicant_progress(
  269. *,
  270. instance_id: str,
  271. actor_user_id: str,
  272. action: str,
  273. task_id: str = "",
  274. ) -> None:
  275. if not _oa_wx_notify_enabled():
  276. logger.info(
  277. "oa wx applicant progress schedule skipped: WX_MP_NOTIFY_ENABLED=false"
  278. )
  279. return
  280. iid = (instance_id or "").strip()
  281. if not iid:
  282. return
  283. result = map_oa_action_to_result(action)
  284. threading.Thread(
  285. target=notify_oa_applicant_progress_sync,
  286. kwargs={
  287. "instance_id": iid,
  288. "actor_user_id": (actor_user_id or "").strip(),
  289. "result": result,
  290. "task_id": (task_id or "").strip(),
  291. },
  292. daemon=True,
  293. name="oa-wx-applicant-progress",
  294. ).start()