"""微信服务号模板消息:OA 审批/抄送通知。""" from __future__ import annotations import logging from typing import Any, Dict import httpx from app.core.config import settings from app.services.wechat_mp_auth import WechatMpAuthError, get_mp_access_token logger = logging.getLogger(__name__) _WX_TEMPLATE_SEND = "https://api.weixin.qq.com/cgi-bin/message/template/send" # 微信 thing 类字段建议约 20 汉字;按字符保守截断 _THING_MAX_CHARS = 20 def _truncate_thing(value: str, max_chars: int = _THING_MAX_CHARS) -> str: s = (value or "").strip() if len(s) <= max_chars: return s return s[:max_chars] def _build_oa_data( *, title: str, applicant: str, node_name: str, time_text: str, process_name: str = "", title_key: str = "", applicant_key: str = "", node_key: str = "", time_key: str = "", process_key: str = "", ) -> Dict[str, Dict[str, str]]: """按字段名组装 data;字段名留空则跳过(兼容临时少字段模板)。""" data: Dict[str, Dict[str, str]] = {} title_key = (title_key or "").strip() applicant_key = (applicant_key or "").strip() node_key = (node_key or "").strip() time_key = (time_key or "").strip() process_key = (process_key or "").strip() if title_key: data[title_key] = {"value": _truncate_thing(title)} if applicant_key: data[applicant_key] = {"value": _truncate_thing(applicant)} if node_key: data[node_key] = {"value": _truncate_thing(node_name)} if time_key: data[time_key] = {"value": (time_text or "").strip()} if process_key: data[process_key] = {"value": _truncate_thing(process_name)} return data def _send_template( *, mp_openid: str, template_id: str, data: Dict[str, Any], pagepath: str, ) -> Dict[str, Any]: oid = (mp_openid or "").strip() if not oid: return {"success": False, "error": "missing_mp_openid"} tid = (template_id or "").strip() if not tid: return {"success": False, "error": "missing_template_id"} mini_appid = (settings.WXPAY_MINI_APPID or "").strip() if not mini_appid: return {"success": False, "error": "missing_mini_appid"} try: access_token = get_mp_access_token() except WechatMpAuthError as exc: logger.warning("mp template send aborted: %s", exc) return {"success": False, "error": str(exc)} path = (pagepath or "").lstrip("/") use_mini_jump = bool(mini_appid and path) payload: Dict[str, Any] = { "touser": oid, "template_id": tid, "data": data, } if use_mini_jump: payload["miniprogram"] = {"appid": mini_appid, "pagepath": path} def _post(body: Dict[str, Any]) -> Dict[str, Any]: with httpx.Client(timeout=15.0) as client: r = client.post( f"{_WX_TEMPLATE_SEND}?access_token={access_token}", json=body, ) r.raise_for_status() return r.json() try: result = _post(payload) except Exception as exc: logger.exception("mp template send request failed") return {"success": False, "error": str(exc)} jumped = use_mini_jump # 服务号未关联小程序 / 页面未发布时微信返回 40165;降级为纯模板通知,保证先可达 if result.get("errcode") == 40165 and use_mini_jump: logger.warning( "mp template miniprogram jump rejected (40165), retry without jump: %s", result.get("errmsg"), ) jumped = False payload_no_jump = { "touser": oid, "template_id": tid, "data": data, } try: result = _post(payload_no_jump) except Exception as exc: logger.exception("mp template fallback send request failed") return {"success": False, "error": str(exc)} if result.get("errcode") in (0, None): logger.info( "mp template send ok: openid=%s msgid=%s jumped=%s", oid, result.get("msgid"), jumped, ) return {"success": True, "msgid": result.get("msgid")} logger.error("mp template send failed: %s", result) return { "success": False, "error": result.get("errmsg") or "send_failed", "errcode": result.get("errcode"), } def send_oa_approval_notify( *, mp_openid: str, pagepath: str, title: str, applicant: str, node_name: str, time_text: str, process_name: str = "", ) -> dict: return _send_template( mp_openid=mp_openid, template_id=settings.WX_MP_TEMPLATE_APPROVAL, data=_build_oa_data( title=title, applicant=applicant, node_name=node_name, time_text=time_text, process_name=process_name, title_key=settings.WX_MP_TPL_FIELD_TITLE, applicant_key=settings.WX_MP_TPL_FIELD_APPLICANT, node_key=settings.WX_MP_TPL_FIELD_NODE, time_key=settings.WX_MP_TPL_FIELD_TIME, process_key=settings.WX_MP_TPL_FIELD_PROCESS, ), pagepath=pagepath, ) def send_oa_cc_notify( *, mp_openid: str, pagepath: str, title: str, applicant: str, node_name: str, time_text: str, process_name: str = "", ) -> dict: return _send_template( mp_openid=mp_openid, template_id=settings.WX_MP_TEMPLATE_CC, data=_build_oa_data( title=title, applicant=applicant, node_name=node_name, time_text=time_text, process_name=process_name, title_key=settings.WX_MP_TPL_CC_FIELD_TITLE, applicant_key=settings.WX_MP_TPL_CC_FIELD_APPLICANT, node_key="", # 抄送模板无当前环节 time_key=settings.WX_MP_TPL_CC_FIELD_TIME, process_key=settings.WX_MP_TPL_CC_FIELD_PROCESS, ), pagepath=pagepath, ) def send_oa_applicant_progress_notify( *, mp_openid: str, pagepath: str, business_name: str, process_name: str, approver_name: str, result: str, time_text: str, ) -> dict: """通知申请人:某步审批结果(const 枚举:通过/失败/转办/其他)。""" data: Dict[str, Dict[str, str]] = {} biz_key = (settings.WX_MP_TPL_PROGRESS_FIELD_BUSINESS or "").strip() process_key = (settings.WX_MP_TPL_PROGRESS_FIELD_PROCESS or "").strip() approver_key = (settings.WX_MP_TPL_PROGRESS_FIELD_APPROVER or "").strip() result_key = (settings.WX_MP_TPL_PROGRESS_FIELD_RESULT or "").strip() time_key = (settings.WX_MP_TPL_PROGRESS_FIELD_TIME or "").strip() if biz_key: data[biz_key] = {"value": _truncate_thing(business_name)} if process_key: data[process_key] = {"value": _truncate_thing(process_name)} if approver_key: data[approver_key] = {"value": _truncate_thing(approver_name)} if result_key: data[result_key] = {"value": (result or "").strip()} if time_key: data[time_key] = {"value": (time_text or "").strip()} return _send_template( mp_openid=mp_openid, template_id=settings.WX_MP_TEMPLATE_APPLICANT_PROGRESS, data=data, pagepath=pagepath, )