diff --git a/backend/app/repositories/reminder_repository.py b/backend/app/repositories/reminder_repository.py index 9b5fbb2..40fee9d 100644 --- a/backend/app/repositories/reminder_repository.py +++ b/backend/app/repositories/reminder_repository.py @@ -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, diff --git a/backend/app/services/bootstrap.py b/backend/app/services/bootstrap.py index 5c30b3d..2973c17 100644 --- a/backend/app/services/bootstrap.py +++ b/backend/app/services/bootstrap.py @@ -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}, ] # --------------------------------------------------------------------------- diff --git a/backend/app/services/wechat_notification_service.py b/backend/app/services/wechat_notification_service.py index d58ceb1..cc90db9 100644 --- a/backend/app/services/wechat_notification_service.py +++ b/backend/app/services/wechat_notification_service.py @@ -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}"