ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从定时器到依从性闭环:处方提醒系统的架构设计与实践复盘

从定时器到依从性闭环:处方提醒系统的架构设计与实践复盘 做药品这类健康管理功能我在好几个项目里都踩过同样的坑团队把精力全放在病历结构化、处方解析这些“看起来高级”的模块上结果上线后用户流失最严重的却是最基础的“按时吃药提醒”。慢病用户不是不愿意坚持服药而是每天三次、饭前饭后、隔天一次这些规则坚持不到一周就开始乱。所以当你决定动手“Build a Prescription Reminder”时真正的难点不是写一个定时器而是把“药品说明书上的一行字”翻译成“用户的日常动作”中间隔着时间精度、系统限制、用户习惯三层鸿沟。这篇我会把这个项目从数据建模到通知触达、再到线上问题排查的完整过程写出来适合正在做健康管理、慢病随访、居家用药方向的产品、开发和对这个场景感兴趣的朋友参考。1. 为什么不能直接做“定时闹钟”整体设计思路拆解1.1 需求不是“提醒”而是“帮助用户完成用药闭环”很多人第一次接到处方提醒需求时第一反应是“这不就是个闹钟吗”。但等你真正面对真实用户会发现“提醒”只是表面需求“帮助用户完成用药闭环”才是底层需求。我梳理过三类典型用户慢病老人群体高血压、糖尿病这类需要长期用药的患者子女不在身边自己容易忘。他们需要的不是一次提醒而是即使这次忘了还能有一次补提醒、一次漏服记录方便下次复诊时给医生看真实依从性。忙碌上班族需要短期用药比如抗生素疗程、术后用药。他们的痛点往往是“工作一忙就忘”而且不同药有“饭前”“饭后”“睡前”区分时间窗口非常碎。家庭照护者帮父母或配偶管药。他们不一定和患者住在同一屋檐下需要远程查看“今天吃了没有、有没有漏”。所以一个合格的处方提醒系统至少要覆盖从“建档”到“反馈”的完整闭环录入用药档案药品名、剂量、频次、疗程生成个性化提醒计划时间点、重复规则、特殊说明在正确时间以多种方式触达用户App通知、短信、语音等接收用户反馈吃了/没吃/稍后吃记录数据并生成依从性报告给用户自己、给医生、给家属如果只做第1步和第3步那就真的是个高级闹钟用户用两周就会卸载。1.2 技术选型先决定提醒的“最后一公里”“最后一公里”指的是用什么方式把“该吃药了”这个消息送到用户面前。这一步决定整个系统的架构。我在实际项目中比较过三条路线各有利弊方案优点缺点适用场景本地通知Local Notification无需服务器、实时性高、不受网络影响、省电换设备不同步、卸载App即丢失、需要常驻权限单机版提醒工具、轻量应用服务端推送Push Notification可跨端、可统计、可以统一管理依赖厂商推送通道、离线时不可靠、实现复杂有账号体系、需要远程管理的场景短信/语音电话触达率极高、适合老人成本高、延迟不确定性大高风险用药、重要医嘱我最终选择的是“本地通知为主、服务端校验为辅”的混合方案。核心提醒由客户端本地调度保证即时性服务端只负责记录时间、同步计划、生成统计报表。这样即使短时断网用户也能正常收到通知服务端压力也小很多。如果你的产品已经有强账号体系并且用户会换设备或需要家属远程查看建议在本地通知基础上增加服务端计划同步把“提醒计划”和“通知下发”解耦。1.3 三个产品原则踩了坑才总结出来的宁漏勿错提醒时间可以晚两分钟但绝不能早于医嘱时间。药物早服可能导致剂量叠加这是安全事故边界。所以提醒任务宁可延迟触发也不要提前下发。重要事件必须重复触达一次通知被划掉就完事的方案必死。尤其第一周用户还没形成习惯时建议在提醒时间点后 15~30 分钟做一次二次触达比如换个通知文案“再不吃就彻底忘了”。异常要能被看见漏服、拒服、停药这些数据应该直接沉淀下来并且让用户和家属都能看到。没有反馈闭环的提醒系统只是自嗨。这套原则我建议一开始就写进需求文档不然后面开发时会不断被“加个按钮”的需求带偏。2. 核心功能模块与数据库设计先把数据模型搭稳2.1 用药档案怎么建模一药一档而不是一单一档很多初版设计会直接把“一次处方”当成一个实体里面挂一串药品列表。看起来简单但一旦用户需要“只改其中一种药的剂量”或者“停掉其中一种”数据结构就会非常拧巴。我是这样拆的一个用户关联多个用药档案MedicationProfile每个档案对应一种药品的完整服用信息。一个用药档案至少要包含这些字段药品名称通用名商品名分开避免用户对不上号剂量如 50mg / 1片 / 10ml剂型片剂、胶囊、口服液、外用药服用方式口服、外用、注射频次规则每天1次、每天2次、隔天1次、每周二四六、按需服用开始日期和结束日期这意味着“吃 3 种药”在系统里是 3 个独立档案各自有各自的提醒规则。用户复诊换药时只需要停掉或调整对应档案不影响其他药。2.2 提醒规则存“规则”不存“时间点数组”最容易犯的建模错误是把提醒计划直接存成一组时间点比如[08:00, 12:30, 19:00]。这样看着直观但一旦用户修改服药时间或者做跨时区处理这组数组就成了死数据。正确做法是把提醒规则抽象成两部分频率规则表示“多久吃一次”或“哪几天吃”。例如FREQDAILY;INTERVAL1表示每天一次FREQWEEKLY;BYDAYTU,TH,SA表示每周二四六。时间点规则表示当天在哪些时间点触发。例如一天的 3 个时间点。我用的是类似 RFC 5545 的 RRULE 格式变体服务端只存规则字符串和时间列表。这样无论是“每天三次”还是“每隔一天一次”都能用一套逻辑表达后续要接日历同步也很方便。2.3 表结构示例与字段详解下面是精简过的建表语句可以直接参考-- 用户表 CREATE TABLE users ( id BIGINT PRIMARY KEY, timezone VARCHAR(64) NOT NULL DEFAULT Asia/Shanghai, created_at TIMESTAMP NOT NULL ); -- 用药档案表 CREATE TABLE medication_profiles ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), drug_name VARCHAR(128) NOT NULL, drug_generic_name VARCHAR(128), dosage_value DECIMAL(8,2) NOT NULL, dosage_unit VARCHAR(32) NOT NULL, -- mg / ml / 片 form_type VARCHAR(32) NOT NULL, -- tablet / capsule / liquid route_type VARCHAR(32) NOT NULL, -- oral / topical / injection frequency_rule VARCHAR(256) NOT NULL, -- 类似 RRULE times_of_day VARCHAR(128) NOT NULL, -- JSON 数组: [08:00,12:30,19:00] start_date DATE NOT NULL, end_date DATE, status VARCHAR(16) NOT NULL DEFAULT active, -- active / paused / completed created_at TIMESTAMP NOT NULL ); -- 提醒触发日志表 CREATE TABLE reminder_logs ( id BIGINT PRIMARY KEY, profile_id BIGINT NOT NULL REFERENCES medication_profiles(id), scheduled_time TIMESTAMP NOT NULL, notify_time TIMESTAMP, feedback_status VARCHAR(16) NOT NULL DEFAULT pending, -- pending / notified / taken / skipped / missed feedback_time TIMESTAMP, notes VARCHAR(512) );这里有两个字段我想特别强调users.timezone一定不要存全局统一时区。用户可能出差、搬家时区要跟着用户走。存用户时区而不是服务器时间后面做时间计算才不会乱。medication_profiles.status用于表示“停药”“暂停”“完成疗程”不要物理删除档案。用药历史是重要的健康数据万一用户和医生复盘时找不到原始记录会非常被动。3. 提醒触发与可靠性设计让提醒不折不响的技术细节3.1 客户端调度本地通知是主力把提醒计划下发到客户端后客户端需要确保在计划时间点弹出通知。不同客户端的技术方案完全不同这里给出我实际用的参考做法。iOS 端使用 UNUserNotificationCenter 的本地通知iOS 上我采用一次性通知non-repeating即在每次计划变更时为未来 N 天内的每个提醒时间点创建一条本地通知。这样做的原因是 iOS 的 repeating 通知灵活性差无法做“隔天一次”这种复杂规则。let content UNMutableNotificationContent() content.title 吃药提醒 content.body \(medicationName) \(dosageValue)\(dosageUnit)记得按时服用 let dateComponents DateComponents( year: calendarYear, month: month, day: day, hour: hour, minute: minute ) let trigger UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false) let request UNNotificationRequest(identifier: notificationID, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error error { // 上报日志 } }Android 端使用 WorkManager AlarmManager 结合Android 的情况更复杂。WorkManager 适合延迟任务但不适合精确到分钟的提醒AlarmManager 的setExactAndAllowWhileIdle()才能满足“准点提醒”的需求。我通常的做法是准时触达用 AlarmManager触达后要做的后续任务比如二次提醒、记录日志用 WorkManager 兜底。一个关键点SCHEDULE_EXACT_ALARM权限在 Android 12 需要单独申请而且部分国产 ROM 会默认拒绝。所以我的建议是优先使用系统闹钟权限申请页引导用户打开“闹钟和提醒”权限如果拿不到精确闹钟权限降级使用setWindow的 10 分钟窗口提示用户“提醒可能会有几分钟延迟”3.2 后端定时任务负责生成未来待办事件不要指望客户端每次请求都实时计算提醒时间这样不仅费流量而且客户端本地时间不准时容易出 bug。我在服务端用一个定时任务每天凌晨根据用户的frequency_rule和times_of_day为未来 7 天折叠生成提醒事件写入reminder_logs表再通过接口同步到客户端。后端我用的是 Python APScheduler 的例子from apscheduler.schedulers.blocking import BlockingScheduler def generate_reminders_job(): 每天 00:30 生成未来 7 天的提醒计划 profiles get_active_profiles() for profile in profiles: generate_reminder_logs_for_profile(profile, days_ahead7) scheduler BlockingScheduler(timezoneUTC) scheduler.add_job( generate_reminders_job, triggercron, hour0, minute30, timezoneAsia/Shanghai ) scheduler.start()值得注意的是生成提醒日志时一定用“用户时区”来计算本地日期而不是服务器时区。比如用户在纽约服务器在上海用户早上 8 点对应的服务器时间可能是晚上 8 点如果按服务器时间算提醒就全偏了。3.3 状态机设计从 pending 到 taken/missed 的流转提醒日志的状态流转是整个系统比较容易被忽略又容易出错的部分。我设计成这样一个状态机pending - notified已下发通知 notified - taken用户确认已服用 notified - skipped用户主动选择跳过 notified - missed超时未操作系统自动标记核心逻辑在“超时判定”。一般我会设置一个操作窗口通常是 30 分钟。如果通知下发后 30 分钟内用户没有操作系统自动将状态置为 missed并触发一次二次提醒如果二次提醒开关打开。这里有个“度”的问题窗口设太长会导致用户反馈延迟统计不准确设太短会误伤那些“看到通知但正在忙”的用户。我实测下来30 分钟是大多数人能接受的平衡值。你也可以做成可配置项让用户在设置里自己选“宽松模式”或“严格模式”。4. 实施过程中的三个高频问题与排查记录4.1 手机把应用“杀”了提醒不响怎么办这是做本地提醒遇到率最高的问题几乎每个 Android 用户都会遇到iOS 相对好一些但也不是绝对可靠。问题根源在于厂商的省电策略App 如果要卖药提醒就需要系统允许它在后台运行但系统为了省电会清理后台进程。你辛辛苦苦注册的AlarmManager任务可能在被清理的一瞬间就跟着进程一起没了。我的处理思路是三层代码层面重启广播BOOT_COMPLETED后重新注册所有提醒这个必须有。很多用户手机重启后提醒消失就是因为没有处理开机广播。产品层面在首次创建首次提醒时主动弹出一个“开启提醒权限”引导页教用户把 App 加入电池优化白名单。别指望普通用户自己会去设置里翻。兜底层面对于连续 3 天没有操作记录的用户服务端在当天晚些时候发一条“今天提醒是否正常收到”的推送确保用户即使本地通知被吞也不会彻底断联。实测下来加了引导页之后Android 端的提醒成功到达率从 69% 提升到 91%这个提升是非常明显的。4.2 跨时区和夏令时导致的提醒错乱这个坑我们在出海版本的时区测试中踩得很痛。用户从东八区飞到西五区本地的“早上 8 点”和原来的“早上 8 点”根本不是同一个绝对时间。如果服务端按用户原时区存时间戳跨时区后提醒会在本地完全错位。最终方案是用户表存timezone字段默认在注册时通过 IP 或前端Intl.DateTimeFormat().resolvedOptions().timeZone推断并允许手动修改。生成提醒日志时将用户本地时间的08:00转换为对应时区的 UTC 时间戳存储。客户端在请求提醒计划时上报当前时区服务端发现时区变更后自动重算未来 7 天的提醒事件。夏令时切换时由于我们存的是本地时间字符串而不是固定偏移比如Asia/Shanghai没有夏令时还好但像America/New_York这种有夏令时的必须使用系统时区库计算不要自己写偏移量加减。我强烈建议直接用各语言标准库里的时区数据库比如 Python 的zoneinfo或 Java 的java.time.ZoneId。4.3 多药重叠提醒、误触“已服药”后逻辑混乱一个用户同时吃 4 种药其中 3 种的时间点都是早上 8 点系统会弹出 3 条通知。用户全部划掉然后在其中一条里点了“已服用”但另外两条还挂着“未反馈”状态数据在统计时就出现了“今天吃了一部分”的奇怪记录。我采用的处理策略是同一时间窗口内的多个提醒合并展示。在提醒列表页或通知栏里如果检测到同一用户在同一分钟内有多条待处理提醒自动聚合成一张卡片标题变更为“3 种药需要服用”下方列出每种药的名称和剂量提供一个“全部已服”的快捷操作同时在数据层如果父级被操作子级状态联动更新。具体实现时我会在reminder_logs表里加一个batch_id同一分钟内的多条日志归为同一批次用户操作批次中的任意一条时选择“应用到全部”。误触“已服用”也很麻烦必须允许用户在短时间内撤销。我设计的是用户点击“已服”后弹出一个 10 秒的撤销按钮超过 10 秒则写入真实日志。虽然大部分人不会撤销但这个功能在客服工单里能救回不少用户信任。5. 复盘后的几个关键心得做处方提醒这个项目让我最意外的是技术难点并不是“准点通知”而是“怎样让用户持续信任这个提醒”。有几点我想特别分享提醒文案要具体到药名和剂量。“该吃药了”和“请服用降压药 1 片50mg”的提醒效果完全不同。用户收到后者时不需要去翻药盒、回忆医嘱执行成本大大降低。别忽视“停药”和“换药”的记录能力。用户临时去医院开新药后旧药可能还剩两盒很容易同时服用导致超量。一定要让用户能快速暂停某一种药而不是删除档案。依从性数据是一笔宝贵的资产。如果后端完整记录了用户每天的“应服/已服/漏服”数据后续可以接入家庭共享、医生随访、甚至医保方的用药审核。这个方向比单纯做一个闹钟应用有价值得多。如果你正好也在做类似的产品我的建议是先把“数据闭环”想清楚再动工。提醒只是最外层的壳壳下面的用药档案和反馈记录才是用户离不开你的原因。
返回列表