| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- """微信服务号模板消息: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,
- )
|