# 患者提醒定时任务设计文档 ## 初步想法 目标与实现思路: - 目标:为在微信小程序中开启提醒功能的患者,按其配置的时间点发送订阅消息推送。 - 调度方式:采用 Spring Task,每分钟触发一次定时任务,定时任务负责从数据库读取提醒配置并判断是否需要发送。 判定与数据来源说明: - 首先检查 `t_patient_reminder` 表中的两项开关: - `is_notification_enabled`(消息总开关), - `is_subscription_available`(一次性订阅开关)。 仅当两者均为 1(开启)时,才继续处理该用户的提醒逻辑。 - 时间点来源: - 测量类(血压/血糖/心率)的时间点保存在对应的 `*_times` 字段(JSON 格式); - 用药提醒的时间点需联动查询 `t_patient_medication` 表中的 `times` 字段(JSON 列表)。 为避免遗漏原始表结构定义,下面以代码块保留原始 CREATE TABLE 内容(未改动): ```sql CREATE TABLE t_patient_reminder ( id bigint(20) NOT NULL COMMENT '主键ID', patient_user_id bigint(20) NOT NULL COMMENT '患者用户ID', is_notification_enabled tinyint(1) DEFAULT '1' COMMENT '是否启用消息通知总开关 (0-禁用, 1-启用)', is_subscription_available tinyint(1) DEFAULT '1' COMMENT '一次性订阅开关 (0-需要重新授权, 1-已授权可发送)', is_blood_pressure_enabled tinyint(1) DEFAULT '1' COMMENT '测量血压提醒开关 (0-禁用, 1-启用)', blood_pressure_times text COLLATE utf8mb4_unicode_ci COMMENT '测量血压的时间点(JSON格式存储)', is_blood_sugar_enabled tinyint(1) DEFAULT '0' COMMENT '测量血糖提醒开关 (0-禁用, 1-启用)', blood_sugar_times text COLLATE utf8mb4_unicode_ci COMMENT '测量血糖的时间点(JSON格式存储)', is_heart_rate_enabled tinyint(1) DEFAULT '1' COMMENT '测量心率提醒开关 (0-禁用, 1-启用)', heart_rate_times text COLLATE utf8mb4_unicode_ci COMMENT '测量心率的时间点(JSON格式存储)', is_medication_enabled tinyint(1) DEFAULT '1' COMMENT '用药提醒开关 (0-禁用, 1-启用)', version int(11) DEFAULT '0' COMMENT '版本号(乐观锁)', create_user bigint(20) DEFAULT NULL COMMENT '创建者ID', create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_user bigint(20) DEFAULT NULL COMMENT '更新者ID', update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', remark varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', PRIMARY KEY (id), UNIQUE KEY uk_patient_user_id (patient_user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='患者提醒设置表'; ``` ```sql CREATE TABLE t_patient_medication ( id bigint(20) NOT NULL COMMENT '主键ID', patient_user_id bigint(20) NOT NULL COMMENT '患者用户ID', medicine_name varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '药品名称', dosage varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '剂量规格', frequency varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '服用频率', times text COLLATE utf8mb4_unicode_ci COMMENT '服用时间点列表(JSON格式存储)', note text COLLATE utf8mb4_unicode_ci COMMENT '备注信息', version int(11) DEFAULT '0' COMMENT '版本号(乐观锁)', create_user bigint(20) DEFAULT NULL COMMENT '创建者ID', create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_user bigint(20) DEFAULT NULL COMMENT '更新者ID', update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', remark varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', PRIMARY KEY (id), KEY idx_patient_user_id (patient_user_id), KEY idx_medicine_name (medicine_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='患者用药记录表'; ``` 以上内容为整理后的实现思路与原始表结构(原文信息全部保留)。后续可按该判定逻辑实现定时任务查询与消息触发。 相关资料: """ 本文件实现了独立的微信订阅消息发送全流程。 包含从配置读取、access_token 获取到消息发送的完整逻辑。 """ import json import logging import os # 患者提醒定时任务设计文档(重构版) **概述**: - **目标**:为已在微信小程序中开启提醒功能的患者,按设定时间点发送订阅消息推送。 - **调度方式**:使用 Spring Task(建议每分钟触发一次),在定时任务中读取数据库,判断需推送的用户并发送消息。 **设计要点**: - **权限与开关判断**:优先判断 `t_patient_reminder` 表中的两项开关:消息总开关(`is_notification_enabled`)与一次性订阅开关(`is_subscription_available`)。两者均为开启时,才继续处理该用户。 - **时间点来源**:测量类(血压/血糖/心率)时间点存储在 `*_times` 字段(JSON),用药提醒时间点需要结合 `t_patient_medication` 表中的 `times` 字段来计算。 - **消息过滤**:只在当前时间点附近(例如±30秒或自定义窗口)发送,避免重复推送。可结合去重(消息历史表或临时缓存)策略。 **处理流程(高层)**: 1. 定时触发(每分钟):获取当前时间点(考虑时区和秒级窗口)。 2. 查询 `t_patient_reminder`,筛选出 `is_notification_enabled=1` 且 `is_subscription_available=1` 的记录。 3. 对每个候选用户: - 解析各项时间配置(血压、血糖、心率、用药等)。 - 若当前时间匹配任一时间点,构造消息负载并调用发送模块。 4. 发送结果记录与错误重试:记录成功/失败,常见失败(如 access_token 失效)尝试重试并适当降级处理(日志告警或入队重试)。 **调度与性能建议**: - 若用户量较大,按用户分片(按 patient_user_id 范围或 hash 分片)并使用并发线程池处理。 - 为防止瞬时大量并发调用微信接口,应在发送端实现速率限制和批量控制。 (本项目为独立小项目,暂不考虑外部队列方案) **数据库字段与含义(摘录说明)** - `t_patient_reminder`:患者提醒设置表,关键字段包含: - `is_notification_enabled`:总开关(0/1) - `is_subscription_available`:一次性订阅开关(0/1) - `is_blood_pressure_enabled`、`blood_pressure_times`:血压开关与时间点(JSON) - `is_blood_sugar_enabled`、`blood_sugar_times`:血糖开关与时间点(JSON) - `is_heart_rate_enabled`、`heart_rate_times`:心率开关与时间点(JSON) - `is_medication_enabled`:用药提醒总开关(用药时间由 `t_patient_medication` 补充) - `t_patient_medication`:患者用药记录表,关键字段包含: - `medicine_name`、`dosage`、`frequency`、`times`(服用时间点 JSON) **微信订阅消息发送模块(架构说明)** - 建议实现一个独立的发送模块,职责包括:读取配置、获取/刷新 `access_token`、执行消息发送、处理接口错误码(如 token 失效重试),并把发送结果上报给定时任务模块。 - 可将该模块封装为一个独立的服务或库(如示例 Python 实现),定时任务只负责构造负载并调用该模块。 **错误处理与监控** - 记录每次发送的响应及异常,针对常见错误(如 `errcode` 表示 token 无效)触发重试逻辑。 - 对失败率、平均延迟等指标建立监控与报警。 --- **附:原始草稿(完整,未改动)** 使用下方代码块可以查看并比对原始内容(未修改)。为完整保留原文,下面使用更长的代码围栏以原样嵌入原文内容: ```` ```markdown # 患者提醒定时任务设计文档 ## 初步想法 我们这个项目还有一个核心功能没有实现,就是为微信小程序开通了提醒功能的用户发送推送消息,我打算用springtask每分钟执行一次定时任务,那么定时任务的话就是我们编写一个工具类,从数据库里获取所有用户的CREATE TABLE t_patient_reminder ( id bigint(20) NOT NULL COMMENT '主键ID', patient_user_id bigint(20) NOT NULL COMMENT '患者用户ID', is_notification_enabled tinyint(1) DEFAULT '1' COMMENT '是否启用消息通知总开关 (0-禁用, 1-启用)', is_subscription_available tinyint(1) DEFAULT '1' COMMENT '一次性订阅开关 (0-需要重新授权, 1-已授权可发送)', is_blood_pressure_enabled tinyint(1) DEFAULT '1' COMMENT '测量血压提醒开关 (0-禁用, 1-启用)', blood_pressure_times text COLLATE utf8mb4_unicode_ci COMMENT '测量血压的时间点(JSON格式存储)', is_blood_sugar_enabled tinyint(1) DEFAULT '0' COMMENT '测量血糖提醒开关 (0-禁用, 1-启用)', blood_sugar_times text COLLATE utf8mb4_unicode_ci COMMENT '测量血糖的时间点(JSON格式存储)', is_heart_rate_enabled tinyint(1) DEFAULT '1' COMMENT '测量心率提醒开关 (0-禁用, 1-启用)', heart_rate_times text COLLATE utf8mb4_unicode_ci COMMENT '测量心率的时间点(JSON格式存储)', is_medication_enabled tinyint(1) DEFAULT '1' COMMENT '用药提醒开关 (0-禁用, 1-启用)', version int(11) DEFAULT '0' COMMENT '版本号(乐观锁)', create_user bigint(20) DEFAULT NULL COMMENT '创建者ID', create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_user bigint(20) DEFAULT NULL COMMENT '更新者ID', update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', remark varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', PRIMARY KEY (id), UNIQUE KEY uk_patient_user_id (patient_user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='患者提醒设置表';,首先检索用户'是否启用消息通知总开关 和 一次性订阅开关是否都处于开启,如果都是开启的则分析用户需要推送的时间点,其中用药提醒的时间点需要联动获取CREATE TABLE t_patient_medication ( id bigint(20) NOT NULL COMMENT '主键ID', patient_user_id bigint(20) NOT NULL COMMENT '患者用户ID', medicine_name varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '药品名称', dosage varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '剂量规格', frequency varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '服用频率', times text COLLATE utf8mb4_unicode_ci COMMENT '服用时间点列表(JSON格式存储)', note text COLLATE utf8mb4_unicode_ci COMMENT '备注信息', version int(11) DEFAULT '0' COMMENT '版本号(乐观锁)', create_user bigint(20) DEFAULT NULL COMMENT '创建者ID', create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_user bigint(20) DEFAULT NULL COMMENT '更新者ID', update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', remark varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', PRIMARY KEY (id), KEY idx_patient_user_id (patient_user_id), KEY idx_medicine_name (medicine_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='患者用药记录表';的数据, 相关资料: """ 本文件实现了独立的微信订阅消息发送全流程。 包含从配置读取、access_token 获取到消息发送的完整逻辑。 """ import json import logging import os import time import requests import certifi class AccessTokenManager: """ 管理微信 access_token 的获取与刷新 """ def __init__(self, appid: str, secret: str): self.appid = appid self.secret = secret def get_access_token(self) -> str: """获取有效的 access_token""" try: url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={self.appid}&secret={self.secret}' response = requests.get(url, timeout=10, verify=certifi.where()) response.raise_for_status() data = response.json() if 'access_token' in data: return data['access_token'] else: raise Exception(f'获取 access_token 失败: {data.get("errmsg", "Unknown error")}') except Exception as e: raise Exception(f'请求 access_token 异常: {str(e)}') class SubscribeMessageSender: """ 发送微信订阅消息的客户端 """ def __init__(self, appid: str, secret: str): self.access_token_manager = AccessTokenManager(appid, secret) self.send_url = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send' def send(self, payload: dict) -> dict: """ 发送订阅消息 Args: payload: 消息负载,必须包含 touser, template_id 等必要字段 Returns: 微信接口返回的响应数据 """ if not isinstance(payload, dict): raise ValueError('payload 必须为字典类型') # 第一次尝试 access_token = self.access_token_manager.get_access_token() result = self._do_send(payload, access_token) # 如果 token 失效,尝试刷新后重试 if result.get('errcode') in (40001, 40014, 42001): logging.info('检测到 access_token 可能失效,尝试刷新后重试') access_token = self.access_token_manager.get_access_token() result = self._do_send(payload, access_token) return result def _do_send(self, payload: dict, access_token: str) -> dict: """执行实际的发送请求""" url = f'{self.send_url}?access_token={access_token}' logging.info(f'发送订阅消息: {url}') logging.debug(f'请求数据: {json.dumps(payload, ensure_ascii=False)}') try: response = requests.post( url, json=payload, timeout=10, verify=certifi.where() ) result = response.json() logging.debug(f'响应数据: {json.dumps(result, ensure_ascii=False)}') return result except Exception as e: logging.error(f'发送请求异常: {str(e)}') return {'errcode': -1, 'errmsg': str(e)} # 使用示例 if __name__ == '__main__': # 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) # 创建发送器实例(直接传入凭证) appid = "wx334b14b3d0bb1547" secret = "0b42a3e4becb7817d08e44e91e2824dd" sender = SubscribeMessageSender(appid, secret) # 构造消息负载 sample_payload = { "touser": "oMrLJ4upWlkcM8ngNnj849sF_sZg", # 用户实际openid "template_id": "ACS7cwcbx0F0Y_YaB4GZr7rWP7BO2-7wQOtYsnUjmFI", # 模板ID 7536 "page": "pages/index/index", # 建议使用完整页面路径 "miniprogram_state": "developer", "lang": "zh_CN", "data": { "phrase2": {"value": "健康打卡"}, # 打卡类型 "date3": {"value": "2025-11-21"}, # 日期 "thing4": {"value": "请按时完成每日健康上报"} # 温馨提示 } } # 发送消息 try: response = sender.send(sample_payload) print('发送结果:', json.dumps(response, ensure_ascii=False, indent=2)) except Exception as e: print('发送失败:', str(e)) 执行结果:PS D:\慢病APP\other\simple-python-server> python send_full_subscribe.py 2025-11-21 17:33:04,374 - INFO - 发送订阅消息: https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=98_0wfkclFyskJQJUWbUYVgvrXbPziTymJ4p4uI2tM-ESMJrv0f8c2smMpZAonBn9k98hfdlVxvG-Ey3flQHwD4vyGOdQH1eKVSka-EJftPTJsB7aaji4iFAmGyV0MUBWcADAYFA 发送结果: { "errcode": 0, "errmsg": "ok", "msgid": 4263515288072994829 } ``` ````