PatientReminderTaskDesign.md 16 KB

患者提醒定时任务设计文档

初步想法

目标与实现思路:

  • 目标:为在微信小程序中开启提醒功能的患者,按其配置的时间点发送订阅消息推送。
  • 调度方式:采用 Spring Task,每分钟触发一次定时任务,定时任务负责从数据库读取提醒配置并判断是否需要发送。

判定与数据来源说明:

  • 首先检查 t_patient_reminder 表中的两项开关:
    • is_notification_enabled(消息总开关),
    • is_subscription_available(一次性订阅开关)。 仅当两者均为 1(开启)时,才继续处理该用户的提醒逻辑。
  • 时间点来源:
    • 测量类(血压/血糖/心率)的时间点保存在对应的 *_times 字段(JSON 格式);
    • 用药提醒的时间点需联动查询 t_patient_medication 表中的 times 字段(JSON 列表)。

为避免遗漏原始表结构定义,下面以代码块保留原始 CREATE TABLE 内容(未改动):

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

患者提醒定时任务设计文档(重构版)

概述

  • 目标:为已在微信小程序中开启提醒功能的患者,按设定时间点发送订阅消息推送。
  • 调度方式:使用 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=1is_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_enabledblood_pressure_times:血压开关与时间点(JSON)
    • is_blood_sugar_enabledblood_sugar_times:血糖开关与时间点(JSON)
    • is_heart_rate_enabledheart_rate_times:心率开关与时间点(JSON)
    • is_medication_enabled:用药提醒总开关(用药时间由 t_patient_medication 补充)
  • t_patient_medication:患者用药记录表,关键字段包含:
    • medicine_namedosagefrequencytimes(服用时间点 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
}
```