import json import time from urllib import error, request from sqlalchemy.orm import Session from backend.app.core.config import get_settings from backend.app.core.error_codes import ErrorCode from backend.app.core.exceptions import AppException class WeChatNotificationService: """微信小程序订阅消息通知服务。 通过微信 subscribeMessage.send 接口向用户推送服务提醒。 需要在小程序后台配置消息模板,并将模板 ID 填入系统配置。 """ TOKEN_CACHE: dict[str, str] = {"token": "", "expires_at": 0} REMINDER_TYPE_TEMPLATE_MAP: dict[str, str] = { "order_status_change": "wechat_template_order_status", "order_approval_needed": "wechat_template_approval", "task_assigned": "wechat_template_task_assigned", "logistics_timeout": "wechat_template_logistics_timeout", "arrears": "wechat_template_arrears", "inactive_customer": "wechat_template_inactive_customer", } def send_subscribe_message(self, open_id: str, reminder_type: str, title: str, content: str, biz_url: str = "") -> bool: settings = get_settings() if not settings.wechat_app_id or not settings.wechat_app_secret: return False template_id = self._get_template_id(settings, reminder_type) if not template_id: return False access_token = self._get_access_token(settings) if not access_token: return False payload = { "touser": open_id, "template_id": template_id, "page": biz_url, "data": { "thing1": {"value": title[:20]}, "thing2": {"value": content[:100]}, "time3": {"value": time.strftime("%Y-%m-%d %H:%M")}, }, } url = f"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={access_token}" req = request.Request( url=url, data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), headers={"Content-Type": "application/json; charset=UTF-8"}, method="POST", ) try: with request.urlopen(req, timeout=10) as response: result = json.loads(response.read().decode("utf-8") or "{}") return result.get("errcode", -1) == 0 except (error.HTTPError, error.URLError, json.JSONDecodeError): return False def bind_open_id(self, session: Session, user_id: int, open_id: str) -> None: from backend.app.models.system import User from sqlalchemy import select user = session.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if user is not None: user.open_id = open_id session.add(user) session.commit() def _get_template_id(self, settings, reminder_type: str) -> str: config_key = self.REMINDER_TYPE_TEMPLATE_MAP.get(reminder_type, "") if not config_key: return "" return getattr(settings, config_key, "") def _get_access_token(self, settings) -> str: now = time.time() if self.TOKEN_CACHE["token"] and self.TOKEN_CACHE["expires_at"] > now + 60: return self.TOKEN_CACHE["token"] url = ( f"https://api.weixin.qq.com/cgi-bin/token" f"?grant_type=client_credential" f"&appid={settings.wechat_app_id}" f"&secret={settings.wechat_app_secret}" ) req = request.Request(url=url, method="GET") try: with request.urlopen(req, timeout=10) as response: result = json.loads(response.read().decode("utf-8") or "{}") token = result.get("access_token", "") expires_in = int(result.get("expires_in", 0)) if token and expires_in > 0: self.TOKEN_CACHE["token"] = token self.TOKEN_CACHE["expires_at"] = now + expires_in return token except (error.HTTPError, error.URLError, json.JSONDecodeError): pass return "" wechat_notification_service = WeChatNotificationService()