去年秋天我妈体检,空腹血糖7.2,复查后医生说是2型糖尿病。从那以后,她每天雷打不动扎手指测血糖,测完就抽一张便利贴写数字贴在冰箱上。一个月下来冰箱门上一排数字,有的旁边写“早上”,有的写“吃完饭”,时间长了根本对不上号。我盯着那堆便利贴突然意识到,家里缺的不是一个记录本,而是一套能替代纸笔、能自动提醒、能直观看出趋势的小工具。这就是我做“2型糖尿病健康管理系统 小程序”的起因。
这套系统的设计目标非常直白:把血糖、饮食、用药、运动四类数据记下来,把数据变成看得懂的图表和报告,在合适的时间提醒用户吃药和复查。它面向的不仅是患者本人,也想帮那些在外工作、没法时刻守着父母的年轻人,提供一个远程了解家人健康状态的窗口。同时这套项目的技术栈并不复杂,很适合正在找微信小程序练手项目、或者做健康类毕业设计的开发者参考。这篇文章我会把从需求拆解、技术选型、数据表设计,到关键代码、订阅消息、上线合规的完整过程都摊开讲一遍,包括我踩过的坑和最后怎么绕过去的。
1. 先从痛点说起:为什么2型糖尿病患者需要一套自己的工具
1.1 纸笔记录的根本问题:数据是死的,没法产生价值
很多没有接触过糖尿病管理的人会问,血糖仪本身不是有记忆功能吗?为什么还要单独做一个系统?这里有个很现实的情况:市面上大多数家用血糖仪只能存几百条记录,导出数据要么靠数据线连电脑,要么靠厂商自家App,而且不同品牌的数据互相不打通。更麻烦的是,一个患者的有效管理,光看血糖值远远不够,还要结合什么时间段测的、这顿饭吃了什么、有没有运动、用药有没有跟上,才能判断某个异常值到底是偶发还是规律性的。
纸笔记录的缺陷在长达几个月的时间尺度上会彻底暴露。便利贴上记一个“5.8”没有上下文,一周之后再回看,你根本想不起来那个数字是空腹还是餐后,更别说去发现“每次吃完午饭两小时血糖都偏高”这种规律。数据一旦变成一堆没有上下文的孤立数字,它就只是记录,不是管理。这也是我最初决定放弃“做一个记录本”这个想法、转而做一个“健康管理系统”的原因。
1.2 这类系统的功能边界:哪些做、哪些坚决不能碰
第一次做医疗健康类项目的人容易犯一个错误,就是想把功能做得特别“全”,全到像在线问诊。这里我必须先把边界划清楚:微信小程序平台对医疗健康类目的审核很严格,普通开发者拿不到互联网诊疗资质,所以系统不能做诊断,不能给治疗方案,不能推荐具体药品。它能做的是记录、提醒、统计、可视化展示,以及基于公开参考区间的“状态提示”——而且提示必须用词谨慎,只能说“偏高”“达标”这类描述,不能写“你有并发症风险”这种结论。
我的做法是,所有异常数据旁边都加一行固定文案:“该结果仅供参考,异常情况请及时咨询内分泌科医生。”同时在隐私政策和版本描述里也反复强调这一点。把边界划清楚不是给自己设限,反而是这个项目能顺利走完审核、上线、长期维护的前提。医疗方向的小程序,合规永远排在功能前面。
2. 技术选型实录:uni-app加uniCloud这套组合是怎么定下来的
2.1 为什么不用原生微信小程序语法
做微信小程序,最保守的方案当然是原生WXML、WXSS、JS,但我在这个项目上一开始就排除了原生方案,核心原因是跨端。这个系统虽然第一版只上微信小程序,但需求随时可能变成“还需要一个支付宝小程序版本”或者“干脆打包成Android App给家里老人装上”。如果用原生语法,到时候等于重写一套。uni-app基于Vue语法,一套代码可以编译到微信小程序、支付宝小程序、H5、App等多个平台,HBuilderX里的开发体验也比较成熟,这是我最看重的一点。
用uni-app还有一个隐形好处:它对Vue开发者几乎没有学习成本。项目里的页面就是一个一个Vue单文件组件,模板、脚本、样式都写在一个文件里,维护起来很直观。我在代码里也大量复用了Vue的组件化思路,比如血糖录入页、趋势图表页、报告页,都是独立组件,后续想加一个“家属查看模式”的页面,只需要复制一个页面骨架再改数据绑定就行。
2.2 云开发解决了后端和数据库的什么问题
这个项目我没有买服务器、没有自己搭后端接口,用的是uniCloud云开发。很多第一次接触云开发的人会问,它和传统“前端 + Node后端 + MySQL”到底差在哪?简单说,云开发把运维那层全砍掉了:你不用配Nginx、不用管SSL证书、不用手动扩容,云函数跑在服务商提供的Node环境里,数据库是现成的文档型数据库,前端直接通过SDK调用。
选择云开发还有一个具体原因:这个项目的用户数据是高度私密的健康数据,传统方案里我得自己设计用户登录态、Token过期、接口鉴权这一套。云函数天然能拿到调用者的openid,而且云数据库支持“仅创建者可读写”这种细粒度权限,我只要在数据表权限配置里打开对应选项,用户A永远不可能通过越权请求读到用户B的血糖记录。这个特性在健康类项目里太重要了,等于服务商帮我兜底了一层数据隔离。
有后端经验的读者可能担心云函数的冷启动延迟。实测下来,uniCloud的云函数在小程序端首次调用大概会有几百毫秒的额外延迟,但可以通过“定时唤起”或者把高频接口合并到一个云函数里来缓解。这个项目的数据量级和调用频率都是个人级,完全没有瓶颈。
2.3 项目目录结构与代码组织
项目整体分两大块:前端页面和云函数。前端用uni-app标准的pages.json配置页面路由和底部TabBar,云函数放在uniCloud的cloudfunctions目录下。我习惯按业务模块拆云函数,而不是一个函数处理所有逻辑,这样每个函数的代码量小、错误定位快。
project-root/ ├── pages/ │ ├── index/ // 首页仪表盘 │ ├── record/ // 血糖/饮食/运动数据录入 │ ├── stats/ // 趋势图表 │ ├── report/ // 健康周报 │ └── mine/ // 个人设置与家属绑定 ├── components/ │ ├── glucose-chart/ // uCharts封装折线图 │ └── status-tag/ // 达标状态标签 ├── utils/ │ ├── convert.js // 血糖单位换算 │ └── format.js // 日期时间格式化 ├── store/ │ └── user.js // 用户状态管理(Vuex) └── uniCloud-aliyun/ └── cloudfunctions/ ├── addRecord/ // 新增健康记录 ├── getWeeklyReport/ // 生成周报 ├── sendReminder/ // 订阅消息定时推送 └── bindFamily/ // 家属绑定这个结构最大的好处是,云函数和前端的边界很清楚。所有数据库操作都走云函数,前端页面不直接写查询逻辑,后续如果想把系统从uniCloud迁到自建后端,只需要重写这一层云函数,页面代码几乎不用动。
3. 核心数据模型设计:血糖、饮食、用药、运动四类记录怎么落库
3.1 血糖记录表:字段设计比你想的更讲究
血糖是整个系统的核心数据,它的表结构直接决定了后面统计功能的实现成本。我第一版设计时把“日期”“时间”拆成两个字段,后来发现不够用,因为“时段”这个属性比“时间”本身更有统计价值。同样是早上八点测的血糖,空腹和早餐后两小时的意义完全不同,所以必须单独用一个枚举字段标记。
| 字段名 | 类型 | 说明 |
|---|---|---|
| _id | String | 记录ID |
| openid | String | 用户唯一标识 |
| value | Number | 血糖值,统一使用mmol/L存储 |
| period | String | 时段枚举值,如fasting |
| measureDate | String | 测量日期,格式YYYY-MM-DD |
| measureTime | String | 测量时间,格式HH:mm |
| note | String | 备注,最长200字 |
| createTime | Number | 创建时间戳 |
这里有一个我反复强调的细节:数据库里血糖值一律用mmol/L存,前端展示时再根据用户偏好换算成mg/dL。原因是统计逻辑只认一套标准,如果数据库里两种单位混存,后续算平均值、看趋势图都得先判断单位,代码复杂度直接翻倍。换算关系很简单,1 mmol/L = 18 mg/dL,我在utils/convert.js里封装了双向转换,用户录入时不管输入哪个单位,存进数据库前统一转成mmol/L。
时段枚举我设计了八个值,覆盖一天中最常见的测量节点:
| 枚举值 | 含义 |
|---|---|
| fasting | 空腹 |
| after_breakfast_1h | 早餐后1小时 |
| after_breakfast_2h | 早餐后2小时 |
| before_lunch | 午餐前 |
| after_lunch_2h | 午餐后2小时 |
| before_dinner | 晚餐前 |
| after_dinner_2h | 晚餐后2小时 |
| bedtime | 睡前 |
这个设计不是为了给用户找麻烦,而是为了后续做“分时段达标率”统计。餐后两小时血糖和空腹血糖在健康管理上的参考意义不同,只有把时段维度保留下来,系统才能回答“患者近两周午餐后血糖控制得怎么样”这类问题。
3.2 饮食、用药、运动记录表的设计思路
血糖值只是结果,饮食、运动、用药是影响结果的原因。这部分记录的设计原则是“够用就好”,不要过度复杂。
饮食记录表我保留了食物名称、进餐时段、碳水化合物估算值和照片可选字段。很多文献都提到碳水化合物摄入对餐后血糖影响最大,所以这个字段我会记录,但不会让用户精确到克,而是在录入表单里给几个挡位:很少(<50g)、正常(50-100g)、较多(>100g)。让患者做精确计算既不现实也会降低使用意愿,粗粒度反而能保证数据持续产生。
用药记录表的设计有一个容易忽略的点:不仅要记录“药名”和“剂量”,还要记录“提醒时间”和“间隔天数”。
| 字段名 | 类型 | 说明 |
|---|---|---|
| drugName | String | 药品名称 |
| dosage | String | 剂量描述 |
| remindTime | String | 每日提醒时间HH:mm |
| dayInterval | Number | 用药间隔天数,1为每天 |
| enabled | Boolean | 是否启用提醒 |
| subscribeCount | Number | 剩余可推送次数 |
运动记录相对最简单,就是运动类型、时长、强度三件套。我额外加了一个“运动时间与进餐的关系”字段,因为餐后适度运动对降低餐后血糖有帮助,特意记录这个维度是想让用户慢慢建立“吃完饭动弹一下”的意识。
3.3 数据权限与安全规则
云数据库的权限设置是健康类项目不能马虎的一环。uniCloud云数据库默认支持多种权限模板,我全部表都选择“仅创建者可读写”。但这里有个关键点:云函数操作数据库不受前端权限限制,所以敏感操作必须放云函数里做,并且云函数内部根据openid强制过滤数据。比如查询历史记录时,前端永远不会直接传“给我读某个ID的记录”,而是只传查询条件,云函数里拿到openid后再拼接完整的查询条件。
另外,健康数据属于敏感个人信息,我不建议在云数据库里明文存真实姓名、手机号这类字段。这个项目用微信授权登录之后,用户表里只保存openid、昵称、头像,以及用户自己填写的年龄、身高、体重这些健康管理必需的信息。昵称和头像也可能包含隐私,我单独给用户提供了“仅家属可见”的开关,默认关闭。
4. 关键功能实现拆解:从表单录入到趋势图表
4.1 血糖录入表单:单位换算与异常值确认
血糖录入是整个系统中最高频的操作,所以它的交互原则只有一个:手指点几下就能完成。我用表单卡片串起所有字段,测量日期默认今天、测量时间默认当前时间、时段默认上一次选择的时段,用户每次打开录入页,需要动的只有“血糖值”这一个输入框。
单位切换用radio-group实现,这也是微信小程序里最基础的单选交互。很多教程会把单位切换做成一个全局开关,但我实测下来,用户家里可能同时有不同品牌的血糖仪,有的显示mmol/L,有的显示mg/dL,所以单位切换放在录入页里更灵活。
<radio-group @change="onUnitChange"> <label class="radio-item"> <radio value="mmol/L" :checked="unit === 'mmol/L'" />mmol/L </label> <label class="radio-item"> <radio value="mg/dL" :checked="unit === 'mg/dL'" />mg/dL </label> </radio-group>提交时的校验逻辑有几个细节值得注意。第一,血糖值必须用type="digit"的输入框,并且提交前做数字类型强转,防止用户输入“5.2.1”这种非法内容。第二,异常值的确认弹窗非常有存在价值。血糖仪的量程一般是1.1-33.3 mmol/L,如果用户输入0.8或者28这种数值,大概率是单位选错或者偶尔一次过低/过高,我会弹窗提示“该数值超出常见量程,确认保存吗”,而不是直接拒绝写入。因为对低血糖患者来说,0.8这种极端值恰恰是需要记录的重要数据。
submitRecord() { const value = Number(this.form.value) if (!value || isNaN(value)) { uni.showToast({ title: '请输入有效的血糖值', icon: 'none' }) return } let mmolValue = value if (this.form.unit === 'mg/dL') { mmolValue = Math.round(value / 18 * 10) / 10 } if (mmolValue < 1.1 || mmolValue > 33.3) { uni.showModal({ title: '数值异常', content: '这个数值超出了常见血糖仪的量程范围,请确认单位是否选对,确认要保存吗?', success: (res) => { if (res.confirm) this.saveToCloud(mmolValue) } }) return } this.saveToCloud(mmolValue) }用户如果选错了单位,比如实际测的是5.8 mmol/L,却填了5.8并选了mg/dL,换算后变成0.3,妥妥触发了量程异常弹窗,用户就会意识到单位选错了。这个设计相当于用数据校验反向约束了输入习惯,实测比单纯放一个单位提示更有效。
4.2 血糖趋势图:图表组件怎么选、Canvas渲染的坑怎么躲
血糖数据的核心价值在趋势,而趋势必须通过图形才能直观传达。图表组件我最终用了基于ucharts封装的qiun-data-charts。选它的原因有四个:一是免费,二是支持微信小程序和App,三是折线图配置足够灵活,四是圆角、颜色、提示框这些样式可以细粒度控制。
折线图的数据结构需要两个数组:一个分类数组(通常是日期),一个series数组(血糖值)。图表组件的入参是标准化的,我只需要把数据库查询结果转换成对应格式。
import uCharts from '@/components/qiun-data-charts/qiun-data-charts.js' // categories: ['04-01', '04-02', '04-03', ...] // series: [{ name: '血糖', data: [5.2, 6.1, 5.8, ...] }] const chartData = { categories: dates, series: [{ name: '血糖', data: values }] }这里有几个真实踩过的坑,我一个个说。
第一个坑是Canvas在真机上的渲染性能。当图表数据点数超过30个时,低端Android真机上会出现明显的绘制延迟,甚至掉帧。我的解决方案是降采样:7天视图只取每天的最高、最低、平均三个点,30天视图取每3天的代表点。趋势图要传达的是变化规律,不是让用户用放大镜找每一个原始值,牺牲少量数据密度换取流畅交互非常值得。
第二个坑和数据库查询有关。uniCloud云数据库单次get()默认最多返回20条记录,需要分页。如果只写一条limit(100)的查询,结果只会返回20条,图表数据就会缺一大截。我封装了一个通用的分页查询函数,用循环翻页直到取完所有数据,再交给图表渲染。
第三个坑是iOS上的组件渲染机制。我把日期时间选择器放在scroll-view里时,在iOS真机上遇到滚动穿透问题:滑动页面时选择器内部也在滚动,导致选择器弹层被页面其他内容盖住,部分区域点不到。解决方法是把选择器弹层用position: fixed定位,并给父容器加catchtouchmove阻止事件穿透。这个现象微信小程序在iOS上比Android更明显,写代码时就要注意。
4.3 健康评估:基于参考区间的状态标注算法
健康评估是连接“记录”和“价值”的关键一环。系统会把每一次血糖值按时段打上状态标签:达标、偏高、偏低,然后统计每个时段在最近7天或30天里的达标率。整个逻辑基于公开可查的血糖管理参考区间,我在代码里明确写了注释:这些数值仅用于健康管理展示,不构成医疗诊断。
const REFERENCE_RANGES = { fasting: { low: 4.4, high: 7.0 }, after_meal_1h: { low: 4.4, high: 10.0 }, after_meal_2h: { low: 4.4, high: 7.8 }, bedtime: { low: 4.4, high: 7.8 } } function evaluate(value, period) { const range = REFERENCE_RANGES[period] if (!range) return 'unknown' if (value < range.low) return 'low' if (value > range.high) return 'high' return 'normal' }周报里我还会展示两个额外指标:一周平均血糖和血糖波动幅度。平均血糖用所有记录值的算术平均数,波动幅度用每天最高值和最低值的差值再取一周平均。这两个指标能直观反映“整体控制水平”和“血糖是不是像过山车”。生成报告的逻辑放在云函数里,因为它涉及多天的数据聚合,在云端跑完直接返回前端展示,可以避免小程序前端处理大量计算。
这里要特别说明,我提到的所有参考区间都只是系统展示用的参考值,不同医院、不同指南的数值可能有差异,所以我的周报页底部一直有一行固定小字:“参考范围仅用于健康管理展示,具体情况请咨询主治医生。”
5. 提醒机制与订阅消息的实战细节
5.1 订阅消息的“一次性授权”问题怎么绕
微信小程序的订阅消息有个让所有开发者头疼的机制:普通模板只能一次性授权,用户点了同意,只能收到一次推送,下一次再发还得再弹窗让用户同意。如果每天都弹一次授权框,用户大概率直接拒绝,然后整个提醒功能就废了。
我试过几种方案,最后留下来的方法是“打卡式续订”:用户在App内吃药打卡时,顺带请求下一次的订阅授权。这个设计的逻辑是,用户正处在“刚完成一次服药动作”的积极状态下,对下一次提醒的接受度最高,此时弹授权框被批准的概率远大于每天早上的随机弹窗。实测下来,这个方案的授权通过率明显高于定时弹窗。
还有一种思路是长期订阅消息。微信确实开放了部分医疗健康类目的长期订阅消息,但申请门槛高,个人开发者基本拿不到。对于这个个人项目,我选择了绕开它而不是死磕它。
5.2 用药提醒的数据结构与其他提醒类型
用药提醒的底层依赖云开发的定时触发器。我在sendReminder云函数里配置了每天定时执行,云函数会查询所有启用了提醒的用药记录,按remindTime匹配当前小时和分钟,命中后调用订阅消息接口推送。
云函数定时触发器的频率以天为单位,没法精确到分钟级,所以我的设计是:定时器每隔半小时跑一次,找出当前时间前后30分钟内需要提醒的记录。考虑到用药提醒本身的精度要求,半小时窗口完全够用。
{ "triggers": [ { "name": "dailyReminder", "type": "timer", "config": "0 0/30 * * * * *" } ] }这里有一个必须处理好的问题:订阅消息的一次性授权意味着云函数手里必须知道“给这个用户还有几次可推送的额度”。我在medication_reminders表里维护了一个subscribeCount字段,每次成功推送一条订阅消息就把这个值减一,减到零就不再尝试推送。用户下一次打卡时,前端调用wx.requestSubscribeMessage请求新授权,云函数再把这个额度补上。这套机制让我能精确控制每一轮的推送次数,不会出现“授权早用完了还在反复推送”的情况。
除了用药提醒,我还做了两种轻量提醒:测量提醒和复查提醒。测量提醒走同样的订阅消息通道,复查提醒则只是首页仪表盘上的一个日期展示,不推消息,避免太多推送让用户产生疲劳。做消息推送一定要克制,过度打扰在工具类产品里等于自杀。
6. 上线流程与合规经验:医疗类小程序比普通工具多走了哪些路
6.1 备案信息怎么填、审核材料准备哪些
现在微信小程序上线前需要完成备案,这是绕不开的一步。我在填写备案信息时,服务内容选择的是“健康管理工具”,而不是“医疗健康服务”,因为我的小程序确实不提供在线问诊、不卖药品,只做记录和展示。备案信息备注里我写了一段很直白的话:“该小程序面向2型糖尿病患者提供血糖、饮食、运动、用药的记录与统计展示功能,所有数据仅供用户自我健康管理参考,不提供任何形式的诊断与治疗建议。”
这段备注不是随便写的,它同时会被小程序类目审核的运营人员看到。备注里把业务范围说得越清楚,审核人员越容易判断你的类目是否匹配。我第一次提交时没有备注清楚,被驳回了一次,要求补充说明业务模式,后来把这段话加进去就顺利通过了。
审核材料方面,除了常规的主体资质,医疗健康类目如果涉及具体医疗功能,需要提供对应的行业资质文件。我这个项目的策略是主动避开需要资质的那些功能,所以只需要常规的企业主体材料。个人开发者也需要注意,微信小程序对个人主体的医疗健康类目限制更严,如果以个人身份注册,很多相关类目是直接不给过的,这也是为什么这个项目最终用公司主体来注册小程序账号。
6.2 隐私政策和用户授权的落地细节
隐私政策不是走过场,尤其对收集健康数据的应用。我在小程序内嵌了一个完整的隐私政策页面,启动时如果用户没有同意过,会先弹窗让用户阅读并主动点击同意。这个逻辑和微信小程序官方的隐私协议弹窗是两套东西,但必须同时做。我方编译时遇到过微信隐私协议检测不通过的情况,表现为开发工具里一切正常,真机调试时报错,检查下来发现是官方的基础库版本太老。把基础库版本调到2.32.3以上、同时在manifest里声明需要使用的隐私接口,问题才解决。
隐私政策的内容我列了五块:收集了哪些信息、为什么收集、存在哪里、谁能看到、用户怎么撤回同意。特别要提到的是,血糖、体重这类数据在用户主动删除记录时,云函数会同步从数据库彻底删除,不留备份。这个承诺写进隐私政策里,开发者就得在代码里真的做到,删除接口要返回成功状态,前端才能给用户明确的反馈。
6.3 真机调试与兼容性测试的心得
这个项目开发过程中我遇到的一个印象很深的真机报错是“登录用户不是该小程序的开发者”。当时用HBuilderX真机运行,在开发工具里一切正常,换到真机上扫码预览就报这个错。排查了半天发现是微信开发者工具的账号和当前试用的小程序AppID不在同一个开发者团队里。解决办法是在微信公众平台的后台把测试微信号加进项目成员列表。
另一个频繁踩坑的地方是iOS手机上底部安全区适配。iPhone X系列之后的小程序页面底部如果没有预留安全区,会出现Home Indicator遮挡按钮的情况。解决方法是在页面底部元素上加padding-bottom: env(safe-area-inset-bottom)。这个细节很多新手会忽略,但实际体验影响很大。
最后说一个编译层面的问题。我用HBuilderX开发时发现,iOS真机的Canvas渲染性能和Android有明显差异,同样的图表组件在iOS上卡顿更明显。除了降采样之外,我还在图表容器设置了固定的canvas-id,避免页面滚动时Canvas被重复创建销毁。测试时一定要准备一台低端Android机和一台旧款iPhone真机,模拟器测不出真实性能。
这个项目从需求整理到上线,前后经历了四五个版本的迭代。每次改动最大的收获不是功能变多了,而是对“边界”的理解更清晰了:医疗健康类工具,少即是多。把记录这件事做到位,把提醒做到不扰人,把数据可视化做到一眼看得懂,就已经完成了它的使命。如果你也在做类似的小程序,先从这三个点出发,不要急着加花哨的功能,你会发现整个开发过程会顺利很多。