wechat_message.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. """微信消息通知服务
  2. 支持微信小程序模板消息推送
  3. 使用方式:
  4. from app.core.wechat_message import WechatMessageService
  5. # 发送订单状态通知
  6. WechatMessageService.send_order_status(
  7. openid="user_openid",
  8. order_no="ORD202603190001",
  9. status="已支付",
  10. amount=100.00
  11. )
  12. # 发送配送通知
  13. WechatMessageService.send_delivery_notice(
  14. openid="deliver_openid",
  15. task_no="DLV20260319001",
  16. address="深圳市南山区...",
  17. time="尽快取货"
  18. )
  19. """
  20. import json
  21. import logging
  22. from typing import Optional, Dict, Any
  23. from datetime import datetime
  24. import requests
  25. from app.core.config import settings
  26. logger = logging.getLogger(__name__)
  27. class WechatMessageService:
  28. """微信消息服务"""
  29. # 模板消息ID配置
  30. TEMPLATE_IDS = {
  31. "order_status": "ORDER_STATUS_TEMPLATE_ID", # 订单状态通知
  32. "delivery_notice": "DELIVERY_NOTICE_TEMPLATE_ID", # 配送通知
  33. "payment_success": "PAYMENT_SUCCESS_TEMPLATE_ID", # 支付成功通知
  34. "refund_result": "REFUND_RESULT_TEMPLATE_ID", # 退款结果通知
  35. "delivery_warning": "DELIVERY_WARNING_TEMPLATE_ID", # 配送时效预警
  36. }
  37. def __init__(self):
  38. self.appid = settings.WXPAY_MINI_APPID
  39. self.secret = settings.WXPAY_SECRET
  40. self.access_token = None
  41. self.token_expires_at = None
  42. def _get_access_token(self) -> Optional[str]:
  43. """获取Access Token"""
  44. if self.access_token and self.token_expires_at:
  45. if datetime.now() < self.token_expires_at:
  46. return self.access_token
  47. try:
  48. url = "https://api.weixin.qq.com/cgi-bin/token"
  49. params = {
  50. "grant_type": "client_credential",
  51. "appid": self.appid,
  52. "secret": self.secret
  53. }
  54. response = requests.get(url, params=params, timeout=10)
  55. result = response.json()
  56. if "access_token" in result:
  57. self.access_token = result["access_token"]
  58. expires_in = result.get("expires_in", 7200)
  59. self.token_expires_at = datetime.now().timestamp() + expires_in - 300
  60. return self.access_token
  61. else:
  62. logger.error(f"获取Access Token失败: {result}")
  63. return None
  64. except Exception as e:
  65. logger.error(f"请求Access Token异常: {str(e)}")
  66. return None
  67. def _send_template_message(self, openid: str, template_id: str, data: Dict, page: str = None, color: str = None) -> Dict:
  68. """发送模板消息
  69. Args:
  70. openid: 用户openid
  71. template_id: 模板ID
  72. data: 模板数据
  73. page: 跳转页面
  74. color: 字体颜色
  75. Returns:
  76. Dict: 发送结果
  77. """
  78. access_token = self._get_access_token()
  79. if not access_token:
  80. return {"success": False, "error": "获取access_token失败"}
  81. try:
  82. url = f"https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={access_token}"
  83. payload = {
  84. "touser": openid,
  85. "template_id": template_id,
  86. "data": data
  87. }
  88. if page:
  89. payload["miniprogram"] = {
  90. "appid": self.appid,
  91. "pagepath": page
  92. }
  93. if color:
  94. payload["color"] = color
  95. response = requests.post(url, json=payload, timeout=10)
  96. result = response.json()
  97. if result.get("errcode") == 0:
  98. logger.info(f"模板消息发送成功: {openid}, msgid: {result.get('msgid')}")
  99. return {"success": True, "msgid": result.get("msgid")}
  100. else:
  101. logger.error(f"模板消息发送失败: {result}")
  102. return {"success": False, "error": result.get("errmsg", "发送失败")}
  103. except Exception as e:
  104. logger.error(f"发送模板消息异常: {str(e)}")
  105. return {"success": False, "error": str(e)}
  106. @classmethod
  107. def send_order_status(
  108. cls,
  109. openid: str,
  110. order_no: str,
  111. status: str,
  112. amount: float,
  113. remark: str = None
  114. ) -> Dict:
  115. """发送订单状态通知
  116. Args:
  117. openid: 用户openid
  118. order_no: 订单号
  119. status: 订单状态
  120. amount: 订单金额
  121. remark: 备注
  122. """
  123. template_id = cls.TEMPLATE_IDS.get("order_status")
  124. if not template_id:
  125. return {"success": False, "error": "未配置订单状态模板ID"}
  126. data = {
  127. "order_no": {"value": order_no, "color": "#173177"},
  128. "status": {"value": status, "color": "#07c160"},
  129. "amount": {"value": f"¥{amount:.2f}", "color": "#ff6600"},
  130. "remark": {"value": remark or "感谢您的使用", "color": "#888888"}
  131. }
  132. service = cls()
  133. return service._send_template_message(
  134. openid=openid,
  135. template_id=template_id,
  136. data=data,
  137. page="pages/client/order/detail/detail"
  138. )
  139. @classmethod
  140. def send_payment_success(
  141. cls,
  142. openid: str,
  143. order_no: str,
  144. amount: float,
  145. payment_method: str = "微信支付"
  146. ) -> Dict:
  147. """发送支付成功通知
  148. Args:
  149. openid: 用户openid
  150. order_no: 订单号
  151. amount: 支付金额
  152. payment_method: 支付方式
  153. """
  154. template_id = cls.TEMPLATE_IDS.get("payment_success")
  155. if not template_id:
  156. return {"success": False, "error": "未配置支付成功模板ID"}
  157. data = {
  158. "order_no": {"value": order_no, "color": "#173177"},
  159. "amount": {"value": f"¥{amount:.2f}", "color": "#ff6600"},
  160. "payment_method": {"value": payment_method, "color": "#07c160"},
  161. "time": {"value": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "color": "#888888"}
  162. }
  163. service = cls()
  164. return service._send_template_message(
  165. openid=openid,
  166. template_id=template_id,
  167. data=data,
  168. page="pages/client/order/detail/detail"
  169. )
  170. @classmethod
  171. def send_delivery_notice(
  172. cls,
  173. openid: str,
  174. task_no: str,
  175. supplier_name: str,
  176. address: str,
  177. remark: str = None
  178. ) -> Dict:
  179. """发送配送通知
  180. Args:
  181. openid: 配送员openid
  182. task_no: 配送单号
  183. supplier_name: 供应商名称
  184. address: 取货地址
  185. remark: 备注
  186. """
  187. template_id = cls.TEMPLATE_IDS.get("delivery_notice")
  188. if not template_id:
  189. return {"success": False, "error": "未配置配送通知模板ID"}
  190. data = {
  191. "task_no": {"value": task_no, "color": "#173177"},
  192. "supplier": {"value": supplier_name, "color": "#07c160"},
  193. "address": {"value": address, "color": "#173177"},
  194. "remark": {"value": remark or "请尽快前往取货", "color": "#ff6600"}
  195. }
  196. service = cls()
  197. return service._send_template_message(
  198. openid=openid,
  199. template_id=template_id,
  200. data=data,
  201. page="pages/deliver/task/detail"
  202. )
  203. @classmethod
  204. def send_delivery_warning(
  205. cls,
  206. openid: str,
  207. task_no: str,
  208. remaining_minutes: int,
  209. urgency_level: str = "紧急"
  210. ) -> Dict:
  211. """发送配送时效预警
  212. Args:
  213. openid: 配送员openid
  214. task_no: 配送单号
  215. remaining_minutes: 剩余分钟数
  216. urgency_level: 紧急程度
  217. """
  218. template_id = cls.TEMPLATE_IDS.get("delivery_warning")
  219. if not template_id:
  220. return {"success": False, "error": "未配置配送预警模板ID"}
  221. color = "#ff0000" if urgency_level == "紧急" else "#ff6600"
  222. data = {
  223. "task_no": {"value": task_no, "color": "#173177"},
  224. "remaining": {"value": f"剩余{remaining_minutes}分钟", "color": color},
  225. "level": {"value": urgency_level, "color": color},
  226. "time": {"value": datetime.now().strftime("%H:%M"), "color": "#888888"}
  227. }
  228. service = cls()
  229. return service._send_template_message(
  230. openid=openid,
  231. template_id=template_id,
  232. data=data,
  233. page="pages/deliver/task/detail"
  234. )
  235. @classmethod
  236. def send_refund_result(
  237. cls,
  238. openid: str,
  239. order_no: str,
  240. refund_amount: float,
  241. refund_time: str = None,
  242. reason: str = None
  243. ) -> Dict:
  244. """发送退款结果通知
  245. Args:
  246. openid: 用户openid
  247. order_no: 订单号
  248. refund_amount: 退款金额
  249. refund_time: 退款时间
  250. reason: 退款原因
  251. """
  252. template_id = cls.TEMPLATE_IDS.get("refund_result")
  253. if not template_id:
  254. return {"success": False, "error": "未配置退款结果模板ID"}
  255. data = {
  256. "order_no": {"value": order_no, "color": "#173177"},
  257. "amount": {"value": f"¥{refund_amount:.2f}", "color": "#ff6600"},
  258. "time": {"value": refund_time or datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "color": "#888888"},
  259. "reason": {"value": reason or "原路退回", "color": "#07c160"}
  260. }
  261. service = cls()
  262. return service._send_template_message(
  263. openid=openid,
  264. template_id=template_id,
  265. data=data,
  266. page="pages/client/refund/refund"
  267. )
  268. @classmethod
  269. def send_urge_notification(
  270. cls,
  271. deliver_openid: str,
  272. order_no: str,
  273. client_name: str,
  274. address: str
  275. ) -> Dict:
  276. """发送催单通知给配送员
  277. Args:
  278. deliver_openid: 配送员openid
  279. order_no: 订单号
  280. client_name: 客户名称
  281. address: 配送地址
  282. """
  283. template_id = cls.TEMPLATE_IDS.get("delivery_notice")
  284. if not template_id:
  285. return {"success": False, "error": "未配置配送通知模板ID"}
  286. data = {
  287. "task_no": {"value": order_no, "color": "#173177"},
  288. "supplier": {"value": f"客户:{client_name}", "color": "#07c160"},
  289. "address": {"value": address, "color": "#173177"},
  290. "remark": {"value": "⚠️ 客户催单,请尽快处理", "color": "#ff0000"}
  291. }
  292. service = cls()
  293. return service._send_template_message(
  294. openid=deliver_openid,
  295. template_id=template_id,
  296. data=data,
  297. page="pages/deliver/task/detail"
  298. )
  299. # 全局实例
  300. wechat_message = WechatMessageService()