| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- """OA 任务创建后服务号通知(commit 后异步)。"""
- from __future__ import annotations
- import logging
- import threading
- from datetime import datetime
- from typing import Iterable, List, Optional
- from urllib.parse import urlencode
- from app.core.config import settings
- from app.core.database import SessionLocal
- from app.models.models import OaApp, OaInstance, OaTask, SysUser
- from app.services.wechat_mp_message import (
- send_oa_applicant_progress_notify,
- send_oa_approval_notify,
- send_oa_cc_notify,
- )
- logger = logging.getLogger(__name__)
- # 微信模板 const4 枚举,必须原样下发
- RESULT_PASS = "通过"
- RESULT_FAIL = "失败"
- RESULT_TRANSFER = "转办"
- RESULT_OTHER = "其他"
- # 模板消息关闭详情后回退的小程序页(与列表入口对齐)
- RETURN_PENDING = "/pages/oa/pending/index"
- RETURN_CC = "/pages/oa/cc/index"
- RETURN_MINE = "/pages/oa/mine/index"
- _ACTION_RESULT = {
- "approve": RESULT_PASS,
- "reject": RESULT_FAIL,
- "transfer": RESULT_TRANSFER,
- "return": RESULT_OTHER,
- "add_sign": RESULT_OTHER,
- }
- def map_oa_action_to_result(action: str) -> str:
- return _ACTION_RESULT.get((action or "").strip(), RESULT_OTHER)
- def build_oa_detail_pagepath(
- *,
- instance_id: str,
- task_id: str = "",
- mode: str,
- return_path: str = "",
- ) -> str:
- """拼小程序 pagepath(无前导 /);query 做 urlencode,保证 returnPath 含斜杠可用。"""
- params = {
- "instanceId": (instance_id or "").strip(),
- "mode": (mode or "view").strip() or "view",
- }
- tid = (task_id or "").strip()
- if tid and tid != "0":
- params["taskId"] = tid
- rp = (return_path or "").strip()
- if rp:
- params["returnPath"] = rp
- return "pages/oa/webview/index?" + urlencode(params)
- def _build_matter_title(*, applicant: str, process_name: str, fallback: str) -> str:
- """申请事项/业务名称:申请人 - 流程名称。"""
- parts = [p for p in [(applicant or "").strip(), (process_name or "").strip()] if p]
- if parts:
- return " - ".join(parts)
- return (fallback or "").strip() or "审批"
- def _resolve_process_and_applicant(db, inst: Optional[OaInstance]):
- applicant = ""
- process_name = ""
- if not inst:
- return applicant, process_name
- if inst.initiator_user_id:
- ini = db.query(SysUser).filter(SysUser.id == inst.initiator_user_id).first()
- applicant = (ini.nick_name if ini else "") or ""
- if inst.app_id:
- app = (
- db.query(OaApp)
- .filter(OaApp.id == inst.app_id, OaApp.del_flag == 0)
- .first()
- )
- process_name = (app.app_name if app else "") or ""
- if not process_name:
- process_name = (inst.title or "").strip()
- return applicant, process_name
- def _send_one(db, task: OaTask) -> None:
- uid = (task.assignee_user_id or "").strip()
- if not uid:
- return
- user = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
- mp = (getattr(user, "wx_mp_openid", None) or "").strip() if user else ""
- if not mp:
- logger.info("oa wx notify skip: no mp_openid user=%s task=%s", uid, task.id)
- return
- inst = (
- db.query(OaInstance)
- .filter(OaInstance.id == task.instance_id, OaInstance.del_flag == 0)
- .first()
- )
- applicant, process_name = _resolve_process_and_applicant(db, inst)
- fallback = (inst.title if inst else "") or (task.node_name or "审批")
- title = _build_matter_title(
- applicant=applicant, process_name=process_name, fallback=fallback
- )
- time_text = datetime.now().strftime("%Y-%m-%d %H:%M")
- node_name = task.node_name or ""
- if task.task_type == "approval" and (task.status or "") == "pending":
- pagepath = build_oa_detail_pagepath(
- instance_id=task.instance_id,
- task_id=task.id,
- mode="approve",
- return_path=RETURN_PENDING,
- )
- send_oa_approval_notify(
- mp_openid=mp,
- pagepath=pagepath,
- title=title,
- applicant=applicant,
- node_name=node_name,
- time_text=time_text,
- process_name=process_name,
- )
- elif task.task_type == "cc":
- pagepath = build_oa_detail_pagepath(
- instance_id=task.instance_id,
- task_id=task.id,
- mode="view",
- return_path=RETURN_CC,
- )
- send_oa_cc_notify(
- mp_openid=mp,
- pagepath=pagepath,
- title=title,
- applicant=applicant,
- node_name=node_name,
- time_text=time_text,
- process_name=process_name,
- )
- def _send_applicant_progress(
- db,
- *,
- instance_id: str,
- actor_user_id: str,
- result: str,
- task_id: str = "",
- ) -> None:
- iid = (instance_id or "").strip()
- if not iid:
- return
- inst = (
- db.query(OaInstance)
- .filter(OaInstance.id == iid, OaInstance.del_flag == 0)
- .first()
- )
- if not inst or not (inst.initiator_user_id or "").strip():
- return
- ini = (
- db.query(SysUser)
- .filter(SysUser.id == inst.initiator_user_id, SysUser.del_flag == 0)
- .first()
- )
- mp = (getattr(ini, "wx_mp_openid", None) or "").strip() if ini else ""
- if not mp:
- logger.info(
- "oa wx applicant progress skip: no mp_openid initiator=%s inst=%s",
- inst.initiator_user_id,
- iid,
- )
- return
- applicant = (ini.nick_name if ini else "") or ""
- process_name = ""
- if inst.app_id:
- app = (
- db.query(OaApp)
- .filter(OaApp.id == inst.app_id, OaApp.del_flag == 0)
- .first()
- )
- process_name = (app.app_name if app else "") or ""
- if not process_name:
- process_name = (inst.title or "").strip()
- business_name = _build_matter_title(
- applicant=applicant,
- process_name=process_name,
- fallback=(inst.title or "审批"),
- )
- actor = None
- aid = (actor_user_id or "").strip()
- if aid:
- actor = (
- db.query(SysUser)
- .filter(SysUser.id == aid, SysUser.del_flag == 0)
- .first()
- )
- approver_name = (actor.nick_name if actor else "") or ""
- pagepath = build_oa_detail_pagepath(
- instance_id=iid,
- task_id="",
- mode="view",
- return_path=RETURN_MINE,
- )
- send_oa_applicant_progress_notify(
- mp_openid=mp,
- pagepath=pagepath,
- business_name=business_name,
- process_name=process_name,
- approver_name=approver_name,
- result=(result or RESULT_OTHER).strip() or RESULT_OTHER,
- time_text=datetime.now().strftime("%Y-%m-%d %H:%M"),
- )
- def _oa_wx_notify_enabled() -> bool:
- return bool(getattr(settings, "WX_MP_NOTIFY_ENABLED", False))
- def notify_oa_tasks_sync(task_ids: Iterable[str]) -> None:
- if not _oa_wx_notify_enabled():
- logger.info("oa wx notify skipped: WX_MP_NOTIFY_ENABLED=false")
- return
- ids = [str(x).strip() for x in task_ids if str(x).strip()]
- if not ids:
- return
- db = None
- try:
- db = SessionLocal()
- tasks = (
- db.query(OaTask)
- .filter(OaTask.id.in_(ids), OaTask.del_flag == 0)
- .all()
- )
- for t in tasks:
- try:
- _send_one(db, t)
- except Exception:
- logger.exception("oa wx notify failed task=%s", t.id)
- except Exception:
- logger.exception("oa wx notify batch failed ids=%s", ids)
- finally:
- if db is not None:
- db.close()
- def notify_oa_applicant_progress_sync(
- *,
- instance_id: str,
- actor_user_id: str,
- result: str,
- task_id: str = "",
- ) -> None:
- if not _oa_wx_notify_enabled():
- logger.info("oa wx applicant progress skipped: WX_MP_NOTIFY_ENABLED=false")
- return
- db = None
- try:
- db = SessionLocal()
- _send_applicant_progress(
- db,
- instance_id=instance_id,
- actor_user_id=actor_user_id,
- result=result,
- task_id=task_id,
- )
- except Exception:
- logger.exception(
- "oa wx applicant progress failed inst=%s actor=%s",
- instance_id,
- actor_user_id,
- )
- finally:
- if db is not None:
- db.close()
- def schedule_oa_task_notifies(task_ids: List[str]) -> None:
- if not _oa_wx_notify_enabled():
- logger.info("oa wx notify schedule skipped: WX_MP_NOTIFY_ENABLED=false")
- return
- ids = [str(x).strip() for x in (task_ids or []) if str(x).strip()]
- if not ids:
- return
- threading.Thread(
- target=notify_oa_tasks_sync, args=(ids,), daemon=True, name="oa-wx-notify"
- ).start()
- def schedule_oa_applicant_progress(
- *,
- instance_id: str,
- actor_user_id: str,
- action: str,
- task_id: str = "",
- ) -> None:
- if not _oa_wx_notify_enabled():
- logger.info(
- "oa wx applicant progress schedule skipped: WX_MP_NOTIFY_ENABLED=false"
- )
- return
- iid = (instance_id or "").strip()
- if not iid:
- return
- result = map_oa_action_to_result(action)
- threading.Thread(
- target=notify_oa_applicant_progress_sync,
- kwargs={
- "instance_id": iid,
- "actor_user_id": (actor_user_id or "").strip(),
- "result": result,
- "task_id": (task_id or "").strip(),
- },
- daemon=True,
- name="oa-wx-applicant-progress",
- ).start()
|