dingdanquanliucheng/backend/app/services/wechat_notification_service.py

212 lines
8.2 KiB
Python

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",
}
# 每个模板的微信字段名映射
# 格式: reminder_type -> { "微信模板字段名": "extra_data 中的 key" }
TEMPLATE_FIELD_MAP: dict[str, dict[str, str]] = {
"order_status_change": {
"phrase3": "status_text",
"time5": "change_time",
"character_string6": "order_no",
"thing4": "remark",
"thing18": "goods_info",
},
"order_approval_needed": {
"thing18": "submitter",
"thing16": "task_name",
"time20": "submit_time",
"thing10": "remark",
},
"task_assigned": {
"time2": "assign_time",
"thing7": "task_name",
"time9": "publish_time",
"time11": "start_time",
},
"logistics_timeout": {
"phrase1": "order_status",
"date2": "deadline",
"thing3": "remark",
"character_string4": "order_no",
},
"arrears": {
"thing5": "order_detail",
"date6": "order_time",
"time10": "appointment_time",
"amount12": "pending_amount",
"character_string4": "order_no",
},
"inactive_customer": {
"thing1": "activity_name",
"time2": "activity_time",
"thing3": "location",
"thing4": "remark",
},
}
def send_subscribe_message(self, open_id: str, reminder_type: str, title: str = "", content: str = "", extra_data: dict | None = None, 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
field_map = self.TEMPLATE_FIELD_MAP.get(reminder_type)
if not field_map:
return False
data = extra_data or {}
now_str = time.strftime("%Y-%m-%d %H:%M")
payload_data = {}
for wx_field, data_key in field_map.items():
value = data.get(data_key, "")
if not value:
value = title if data_key == "order_no" else now_str
payload_data[wx_field] = {"value": str(value)[:100]}
payload = {
"touser": open_id,
"template_id": template_id,
"page": biz_url,
"data": payload_data,
}
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
async def send_reminder(self, open_id: str, reminder_type: str, data: dict | None = None) -> bool:
"""发送提醒消息(兼容 event_bus 调用)
Args:
open_id: 用户的微信 open_id
reminder_type: 提醒类型
data: 额外数据
Returns:
是否发送成功
"""
extra_data = {}
if data:
# 从 event_bus 传递的数据中提取模板字段所需的数据
extra_data = {
"order_no": data.get("order_no", ""),
"status_text": data.get("status_desc", data.get("status", "")),
"change_time": data.get("change_time", time.strftime("%Y-%m-%d %H:%M")),
"remark": data.get("remark", data.get("content", "")),
"goods_info": data.get("goods_info", ""),
"submitter": data.get("salesman_name", data.get("submitter", "")),
"task_name": data.get("task_no", data.get("task_name", "")),
"submit_time": data.get("submit_time", time.strftime("%Y-%m-%d %H:%M")),
"assign_time": data.get("assign_time", time.strftime("%Y-%m-%d %H:%M")),
"publish_time": data.get("publish_time", time.strftime("%Y-%m-%d %H:%M")),
"start_time": data.get("start_time", time.strftime("%Y-%m-%d %H:%M")),
"order_status": data.get("status_desc", data.get("status", "")),
"deadline": data.get("deadline", ""),
"order_detail": data.get("order_detail", data.get("content", "")),
"order_time": data.get("order_time", ""),
"appointment_time": data.get("appointment_time", ""),
"pending_amount": data.get("amount", ""),
"activity_name": data.get("customer_name", ""),
"activity_time": data.get("activity_time", time.strftime("%Y-%m-%d %H:%M")),
"location": data.get("location", ""),
}
title = data.get("title", "") if data else ""
content = data.get("content", "") if data else ""
biz_url = data.get("biz_url", "") if data else ""
return self.send_subscribe_message(
open_id=open_id,
reminder_type=reminder_type,
title=title,
content=content,
extra_data=extra_data,
biz_url=biz_url
)
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()