feat: 微信订阅消息模板字段适配,填入 6 个真实模板 ID

- bootstrap.py 写入 6 个微信订阅消息模板 ID
- wechat_notification_service 新增 TEMPLATE_FIELD_MAP 按模板字段名发送
- reminder_repository 新增 _build_extra_data 从 title/content 提取结构化数据

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
taiyi 2026-06-06 13:25:10 +08:00
parent 8c8d730184
commit 1a3b67c423
3 changed files with 128 additions and 14 deletions

View File

@ -5,6 +5,7 @@
ReminderService提醒服务调用
"""
import re
from datetime import datetime
from sqlalchemy import and_, func, select
@ -110,15 +111,77 @@ class ReminderRepository:
user = session.execute(select(User).where(User.id == receiver_id)).scalar_one_or_none()
if user is None or not user.open_id:
return
extra_data = self._build_extra_data(reminder)
wechat_notification_service.send_subscribe_message(
user.open_id,
reminder.reminder_type,
reminder.reminder_title,
reminder.reminder_content,
extra_data=extra_data,
)
except Exception:
pass
def _build_extra_data(self, reminder: SystemReminder) -> dict:
"""根据提醒类型从 title/content 中提取结构化数据,用于填充微信模板字段。
参数:
reminder: 系统提醒对象
返回:
包含模板所需字段数据的字典
"""
title = reminder.reminder_title or ""
content = reminder.reminder_content or ""
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
order_no_match = re.search(r'[\d-]{10,}', title + content)
order_no = order_no_match.group() if order_no_match else title
base: dict[str, str] = {
"order_no": order_no,
"change_time": now_str,
"assign_time": now_str,
"timeout_time": now_str,
"check_time": now_str,
"submit_time": now_str,
}
rtype = reminder.reminder_type
if rtype == "order_status_change":
base["status_text"] = content[:50] if content else title
elif rtype == "order_approval_needed":
base["submitter"] = "业务员"
base["order_info"] = title
base["remark"] = "请尽快审批"
elif rtype == "task_assigned":
base["task_no"] = order_no
base["task_type"] = "运输任务"
base["assign_date"] = now_str.split(" ")[0]
elif rtype == "logistics_timeout":
hours_match = re.search(r'(\d+)\s*小时', content)
base["timeout_info"] = f"超过{hours_match.group(1) if hours_match else '48'}小时未流转"
base["remark"] = "请及时跟进"
elif rtype == "arrears":
amount_match = re.search(r'([\d.]+)\s*元', content)
customer_match = re.search(r'客户[:](.+?)[,]', content)
base["customer_info"] = (customer_match.group(1) if customer_match else "") + "欠款"
base["arrears_amount"] = amount_match.group(1) if amount_match else "0"
base["order_date"] = now_str
base["due_date"] = ""
elif rtype == "inactive_customer":
days_match = re.search(r'(\d+)\s*天', content)
last_time_match = re.search(r'最近订单时间[:](.+?)[。,]', content)
base["customer_name"] = title.replace("沉默客户提醒 - ", "")
base["inactive_info"] = f"超过{days_match.group(1) if days_match else '90'}天未下单"
base["last_order_time"] = last_time_match.group(1).strip() if last_time_match else "暂无"
return base
def find_active_reminder(
self,
session: Session,

View File

@ -41,12 +41,12 @@ DEFAULT_CONFIGS = [
{"config_key": "allow_cancel_statuses", "config_value": "draft,pending_approve,approved,pending_factory", "config_name": "允许取消的订单状态", "remark": "逗号分隔的状态编码列表", "status": 1},
{"config_key": "report_category_mapping", "config_value": "{}", "config_name": "报表分类映射", "remark": "按产品分类配置统计口径", "status": 1},
{"config_key": "logistics_trace_provider", "config_value": "internal", "config_name": "物流轨迹服务商", "remark": "internal=内置kdniao=快递100", "status": 1},
{"config_key": "wechat_template_order_status", "config_value": "", "config_name": "微信模板-订单状态变更", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_approval", "config_value": "", "config_name": "微信模板-审批通知", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_task_assigned", "config_value": "", "config_name": "微信模板-任务分配", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_logistics_timeout", "config_value": "", "config_name": "微信模板-物流超时", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_arrears", "config_value": "", "config_name": "微信模板-欠款提醒", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_inactive_customer", "config_value": "", "config_name": "微信模板-沉默客户", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_order_status", "config_value": "lA4NkXqSYHXr4XvULlmJUSqS1pM8dFrYq5_poA-bM7g", "config_name": "微信模板-订单状态变更", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_approval", "config_value": "Zcd8dxVRBPGpQIO1fhMjgWDy2mS012KoOeKbJK9mrAk", "config_name": "微信模板-审批通知", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_task_assigned", "config_value": "rz5ndHgRxaqCpikfH0qYFMA1w5YeT1IZ6b19e9F1W5c", "config_name": "微信模板-任务分配", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_logistics_timeout", "config_value": "N07iQmxpk3iaAyRNqy7NdUUNAXD0dDaE99DcbpgSwrM", "config_name": "微信模板-物流超时", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_arrears", "config_value": "uSSHcFChGkJSSGj9XwjVTUiZ51yWuaPMRYeLY9nrJME", "config_name": "微信模板-欠款提醒", "remark": "微信小程序订阅消息模板ID", "status": 1},
{"config_key": "wechat_template_inactive_customer", "config_value": "B0htDO1tdyFCXXpw7lQaK5dcDkcUtrRNdJpWcsc7T3M", "config_name": "微信模板-沉默客户", "remark": "微信小程序订阅消息模板ID", "status": 1},
]
# ---------------------------------------------------------------------------

View File

@ -27,7 +27,49 @@ class WeChatNotificationService:
"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:
# 每个模板的微信字段名映射
# 格式: reminder_type -> { "微信模板字段名": "extra_data 中的 key" }
TEMPLATE_FIELD_MAP: dict[str, dict[str, str]] = {
"order_status_change": {
"thing1": "order_no",
"thing2": "status_text",
"time3": "change_time",
"thing4": "order_no",
},
"order_approval_needed": {
"thing1": "submitter",
"thing2": "order_info",
"time3": "submit_time",
"thing5": "remark",
},
"task_assigned": {
"thing1": "task_no",
"thing2": "task_type",
"date3": "assign_date",
"time5": "assign_time",
},
"logistics_timeout": {
"thing1": "order_no",
"thing2": "timeout_info",
"time3": "timeout_time",
"thing4": "remark",
},
"arrears": {
"time1": "order_date",
"thing2": "customer_info",
"amount3": "arrears_amount",
"time4": "due_date",
"thing5": "order_no",
},
"inactive_customer": {
"thing1": "customer_name",
"thing2": "inactive_info",
"thing3": "last_order_time",
"time4": "check_time",
},
}
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
@ -40,15 +82,24 @@ class WeChatNotificationService:
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": {
"thing1": {"value": title[:20]},
"thing2": {"value": content[:100]},
"time3": {"value": time.strftime("%Y-%m-%d %H:%M")},
},
"data": payload_data,
}
url = f"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={access_token}"