"""微信消息通知服务 支持微信小程序模板消息推送 使用方式: from app.core.wechat_message import WechatMessageService # 发送订单状态通知 WechatMessageService.send_order_status( openid="user_openid", order_no="ORD202603190001", status="已支付", amount=100.00 ) # 发送配送通知 WechatMessageService.send_delivery_notice( openid="deliver_openid", task_no="DLV20260319001", address="深圳市南山区...", time="尽快取货" ) """ import json import logging from typing import Optional, Dict, Any from datetime import datetime import requests from app.core.config import settings logger = logging.getLogger(__name__) class WechatMessageService: """微信消息服务""" # 模板消息ID配置 TEMPLATE_IDS = { "order_status": "ORDER_STATUS_TEMPLATE_ID", # 订单状态通知 "delivery_notice": "DELIVERY_NOTICE_TEMPLATE_ID", # 配送通知 "payment_success": "PAYMENT_SUCCESS_TEMPLATE_ID", # 支付成功通知 "refund_result": "REFUND_RESULT_TEMPLATE_ID", # 退款结果通知 "delivery_warning": "DELIVERY_WARNING_TEMPLATE_ID", # 配送时效预警 } def __init__(self): self.appid = settings.WXPAY_MINI_APPID self.secret = settings.WXPAY_SECRET self.access_token = None self.token_expires_at = None def _get_access_token(self) -> Optional[str]: """获取Access Token""" if self.access_token and self.token_expires_at: if datetime.now() < self.token_expires_at: return self.access_token try: url = "https://api.weixin.qq.com/cgi-bin/token" params = { "grant_type": "client_credential", "appid": self.appid, "secret": self.secret } response = requests.get(url, params=params, timeout=10) result = response.json() if "access_token" in result: self.access_token = result["access_token"] expires_in = result.get("expires_in", 7200) self.token_expires_at = datetime.now().timestamp() + expires_in - 300 return self.access_token else: logger.error(f"获取Access Token失败: {result}") return None except Exception as e: logger.error(f"请求Access Token异常: {str(e)}") return None def _send_template_message(self, openid: str, template_id: str, data: Dict, page: str = None, color: str = None) -> Dict: """发送模板消息 Args: openid: 用户openid template_id: 模板ID data: 模板数据 page: 跳转页面 color: 字体颜色 Returns: Dict: 发送结果 """ access_token = self._get_access_token() if not access_token: return {"success": False, "error": "获取access_token失败"} try: url = f"https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={access_token}" payload = { "touser": openid, "template_id": template_id, "data": data } if page: payload["miniprogram"] = { "appid": self.appid, "pagepath": page } if color: payload["color"] = color response = requests.post(url, json=payload, timeout=10) result = response.json() if result.get("errcode") == 0: logger.info(f"模板消息发送成功: {openid}, msgid: {result.get('msgid')}") return {"success": True, "msgid": result.get("msgid")} else: logger.error(f"模板消息发送失败: {result}") return {"success": False, "error": result.get("errmsg", "发送失败")} except Exception as e: logger.error(f"发送模板消息异常: {str(e)}") return {"success": False, "error": str(e)} @classmethod def send_order_status( cls, openid: str, order_no: str, status: str, amount: float, remark: str = None ) -> Dict: """发送订单状态通知 Args: openid: 用户openid order_no: 订单号 status: 订单状态 amount: 订单金额 remark: 备注 """ template_id = cls.TEMPLATE_IDS.get("order_status") if not template_id: return {"success": False, "error": "未配置订单状态模板ID"} data = { "order_no": {"value": order_no, "color": "#173177"}, "status": {"value": status, "color": "#07c160"}, "amount": {"value": f"¥{amount:.2f}", "color": "#ff6600"}, "remark": {"value": remark or "感谢您的使用", "color": "#888888"} } service = cls() return service._send_template_message( openid=openid, template_id=template_id, data=data, page="pages/client/order/detail/detail" ) @classmethod def send_payment_success( cls, openid: str, order_no: str, amount: float, payment_method: str = "微信支付" ) -> Dict: """发送支付成功通知 Args: openid: 用户openid order_no: 订单号 amount: 支付金额 payment_method: 支付方式 """ template_id = cls.TEMPLATE_IDS.get("payment_success") if not template_id: return {"success": False, "error": "未配置支付成功模板ID"} data = { "order_no": {"value": order_no, "color": "#173177"}, "amount": {"value": f"¥{amount:.2f}", "color": "#ff6600"}, "payment_method": {"value": payment_method, "color": "#07c160"}, "time": {"value": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "color": "#888888"} } service = cls() return service._send_template_message( openid=openid, template_id=template_id, data=data, page="pages/client/order/detail/detail" ) @classmethod def send_delivery_notice( cls, openid: str, task_no: str, supplier_name: str, address: str, remark: str = None ) -> Dict: """发送配送通知 Args: openid: 配送员openid task_no: 配送单号 supplier_name: 供应商名称 address: 取货地址 remark: 备注 """ template_id = cls.TEMPLATE_IDS.get("delivery_notice") if not template_id: return {"success": False, "error": "未配置配送通知模板ID"} data = { "task_no": {"value": task_no, "color": "#173177"}, "supplier": {"value": supplier_name, "color": "#07c160"}, "address": {"value": address, "color": "#173177"}, "remark": {"value": remark or "请尽快前往取货", "color": "#ff6600"} } service = cls() return service._send_template_message( openid=openid, template_id=template_id, data=data, page="pages/deliver/task/detail" ) @classmethod def send_delivery_warning( cls, openid: str, task_no: str, remaining_minutes: int, urgency_level: str = "紧急" ) -> Dict: """发送配送时效预警 Args: openid: 配送员openid task_no: 配送单号 remaining_minutes: 剩余分钟数 urgency_level: 紧急程度 """ template_id = cls.TEMPLATE_IDS.get("delivery_warning") if not template_id: return {"success": False, "error": "未配置配送预警模板ID"} color = "#ff0000" if urgency_level == "紧急" else "#ff6600" data = { "task_no": {"value": task_no, "color": "#173177"}, "remaining": {"value": f"剩余{remaining_minutes}分钟", "color": color}, "level": {"value": urgency_level, "color": color}, "time": {"value": datetime.now().strftime("%H:%M"), "color": "#888888"} } service = cls() return service._send_template_message( openid=openid, template_id=template_id, data=data, page="pages/deliver/task/detail" ) @classmethod def send_refund_result( cls, openid: str, order_no: str, refund_amount: float, refund_time: str = None, reason: str = None ) -> Dict: """发送退款结果通知 Args: openid: 用户openid order_no: 订单号 refund_amount: 退款金额 refund_time: 退款时间 reason: 退款原因 """ template_id = cls.TEMPLATE_IDS.get("refund_result") if not template_id: return {"success": False, "error": "未配置退款结果模板ID"} data = { "order_no": {"value": order_no, "color": "#173177"}, "amount": {"value": f"¥{refund_amount:.2f}", "color": "#ff6600"}, "time": {"value": refund_time or datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "color": "#888888"}, "reason": {"value": reason or "原路退回", "color": "#07c160"} } service = cls() return service._send_template_message( openid=openid, template_id=template_id, data=data, page="pages/client/refund/refund" ) @classmethod def send_urge_notification( cls, deliver_openid: str, order_no: str, client_name: str, address: str ) -> Dict: """发送催单通知给配送员 Args: deliver_openid: 配送员openid order_no: 订单号 client_name: 客户名称 address: 配送地址 """ template_id = cls.TEMPLATE_IDS.get("delivery_notice") if not template_id: return {"success": False, "error": "未配置配送通知模板ID"} data = { "task_no": {"value": order_no, "color": "#173177"}, "supplier": {"value": f"客户:{client_name}", "color": "#07c160"}, "address": {"value": address, "color": "#173177"}, "remark": {"value": "⚠️ 客户催单,请尽快处理", "color": "#ff0000"} } service = cls() return service._send_template_message( openid=deliver_openid, template_id=template_id, data=data, page="pages/deliver/task/detail" ) # 全局实例 wechat_message = WechatMessageService()