去年下半年我接了一个社区健康服务的项目,需求方是某街道的居家养老服务中心。他们之前一直用纸质台账加微信群管理辖区老人的健康档案,血压血糖数据靠老人手抄在本子上,随访靠打电话,用药提醒靠子女自觉。接手之前我也觉得健康类小程序无非是"信息展示加预约挂号"那一套,真正跑进社区蹲了两周之后才发现,这个场景比想象中复杂得多——老人操作能力参差不齐、数据高度敏感、子女远程关心、社区医生需要批量随访,每一条都在挑战技术选型和交互设计。
最终交付的是一个微信小程序 + Python Flask 的完整系统。小程序端负责老人和家属的使用入口,Flask后端提供健康档案、体检数据、用药提醒、上门随访预约等API。这篇文章把整个项目的决策过程、落地细节和踩过的坑完整写出来,特别是那些在官方文档里查不到、只有实际联调才会遇到的细节,希望能给正在做类似社区健康系统的朋友一些参考。
1. 社区健康管理这个场景,为什么非要一套专属系统
1.1 社区一线的真实痛点
健康管理系统的需求方通常不是老人本人,而是社区工作人员。他们的日常工作状态是这样的:每个季度组织一次集中体检,体检报告打印出来装进档案袋;平时老人自己在家测血压血糖,数据靠手写记录;医生上门随访之前,需要翻半天纸质档案才知道这个老人有没有高血压、药物过敏史。这种模式最致命的问题不是效率低,而是信息不连续——老人去了趟医院,医生开了新药,社区这边完全不知情;老人记性不好忘了吃降压药,社区也无从知晓。
我调研的那个社区,辖区内有1600多位65岁以上老人,其中独居和空巢的接近300人。工作人员只有4名,日常服务已经排满,根本做不到主动关注每个人的异常。他们的核心诉求是:能不能有一套系统,让老人和家属把日常健康数据传上来,社区医生能快速看到异常波动,然后上门或者电话干预。这个诉求说白了就是要一个"数据汇集的漏斗"——层层的健康信息从千家万户汇集到社区服务端,再由工作人员做筛选和处置。
1.2 为什么载体必须是微信小程序
调研阶段我也考虑过H5网页和独立App,最后全部否掉了。原因很现实:
- 老人不会装App,但几乎人人用微信。对65岁以上人群来说,在应用商店里搜索、下载、注册、登录是四道坎,每道坎都能劝退一半人。微信小程序扫一扫就能进,不用安装,这个门槛差异是决定性的。
- 子女是实际的高频操作者。很多老人自己不用手机,但子女会用微信给父母代办。小程序分享给子女,点开就能用,整个"代操作"的链路顺畅得多。
- 小程序提供的能力足够用。蓝牙(对接血压计)、定位(紧急求助)、订阅消息(用药提醒模板)这些原生能力都具备,未来扩展硬件对接不需要换载体。
后来实际运营数据也验证了这个判断——系统上线后的用户中,60岁以上老人自主操作的占比只有约三成,其余七成由子女或社工代为操作。如果当初选了App,这个比例只会更低。
1.3 系统的核心功能边界
跟需求方反复对齐之后,功能范围收敛为六大模块:老人及家属账号体系、健康档案管理(基础信息、病史、过敏史)、日常体征数据录入(血压、血糖、心率)、用药提醒与打卡、体检报告归档与趋势展示、社区医生随访预约。每个模块都要求"家属能看、老人能录、医生能管"三个角色视角。
这个边界确认花了不少时间。需求方一开始提了很多想法,比如在线问诊、送药上门、远程视频会诊,全部砍掉了。社区级服务系统的原则是"不做医疗行为,只做信息服务"——系统负责记录、提醒、展示,不提供诊断和治疗建议,否则涉及医疗资质问题,项目根本没法落地。这一点在早期跟需求方讲清楚很重要,否则后期需求蔓延会非常痛苦。
2. 技术路线定夺:Flask + 微信小程序组合的取舍过程
2.1 Flask与FastAPI的对比:不是越"新"越好
后端选型时,团队里有同事提议用FastAPI,理由是性能好、自动生成OpenAPI文档、原生异步。我专门花了一个下午做了对比测试和评估,最终坚持用Flask,原因有几个层面。
| 对比维度 | Flask | FastAPI |
|---|---|---|
| 同步CRUD开发效率 | 高,生态成熟,文档多 | 高,但异步特性对初级开发者有认知负担 |
| 社区资源 | 10年+积累,踩坑案例丰富 | 较新,部分中年件兼容问题需要自己趟 |
| 部署运维 | gunicorn + nginx 经典组合,方案极其成熟 | 同样支持,但uvicorn + 异步worker的调优经验相对少 |
| 团队上手成本 | 低,任何会Python的都能立刻上手 | 中,需要理解async/await和事件循环 |
| 本项目实际瓶颈 | 无,业务是简单CRUD加少量定时任务 | 无,杀鸡用牛刀 |
这个系统的最大性能瓶颈不在后端,而在社区宽带环境和4G网络下的请求往返。Flask的同步模型配合gunicorn多worker,QPS撑到几百完全没问题,而我们的实际并发量峰值可能不到20。选FastAPI带来的性能提升在这个场景里毫无感知,反而增加了部署和排障的复杂度。我后来总结了一句话:技术选型看的是团队维护能力和业务真实瓶颈,不是框架的benchmark分数。
2.2 前后端分离还是混搭
这个项目我采用了前后端完全分离的架构:微信小程序端只负责展示和收集数据,所有业务逻辑全部在Flask后端;前端通过wx.request调用后端API,数据格式统一为JSON。后端不关心页面长什么样,只输出结构化数据。
为什么这么设计?因为微信小程序的发版审核机制比较特殊,每次改动都需要提交审核,审核周期最短一两天,加急也就半天。如果把业务判断逻辑写在小程序端,那每次调整都要触发一次发版审核,节奏非常慢。而后端的更新是即时的,改完重启服务就生效。所以原则是:能放后端的判断一律放后端。比如"血糖值超过11.1mmol/L判定为异常需要提醒"这个逻辑,明显放后端,前端只负责把数值传上来,后台判定后再决定是否返回异常提醒标记。
2.3 数据模型设计的几个关键点
数据库用的MySQL,表结构设计花了比较多心思,核心几张表如下:
- user表:用户基础信息,通过role字段区分老人、家属、社区医生三种角色;家属与老人通过elder_id建立绑定关系。
- elder_info表:老人的详细档案,包括既往病史、过敏史、紧急联系人、常用药物清单,这些字段为了灵活采用了JSON类型存储。
- health_record表:日常体征数据,type字段区分blood_pressure、blood_glucose、heart_rate,value存储具体的数值,unit字段带单位。
- medication_reminder表:用药提醒,包括药品名称、剂量、提醒时间、老人id;每次打卡记录写medication_log表。
- appointment表:随访预约,关联老人、医生、时间、状态和备注。
设计时有两点值得分享。第一,体征数据表的unit字段一定要保留。血压的mmHg和血糖的mmol/L,不同医院体检报告可能用不同单位,保留原始单位方便后期换算。第二,异常判定阈值不要写死在代码里,建一张medical_alert_config表,社区医生可以自己在后台调整阈值。我开发完第一期之后发现,医生对"血压偏高"的理解和指标书的参考值并不完全一致,有人觉得收缩压140就该提醒,有人觉得160才算异常,这种差异必须用配置化去解决,否则后期需求变更会改代码改到崩溃。
3. 先啃硬骨头:微信小程序端的登录、导航与适老交互
3.1 从wx.login到手机号获取的完整链路
微信小程序的登录流程是第一个坑。早期的getPhoneNumber接口可以直接在open-type="getPhoneNumber"的按钮回调里拿到手机号,但从某个版本开始,这个接口返回的加密数据需要后端调用code2Session接口,并且要配合小程序后台的"获取手机号"权限申请才能使用,而且个人主体小程序直接被禁止获取手机号,必须企业主体或个体户主体。
整个登录链路的最终实现方案是这样的:
- 小程序端调用wx.login拿到临时code;
- 使用button组件,open-type="getPhoneNumber",用户点击授权后返回encryptedData和iv;
- 前端把code、encryptedData、iv一起POST到后端/api/login接口;
- Flask后端用appid和secret调用微信的jscode2session接口,换取openid和session_key;
- 用session_key解密手机号数据,从返回值中拿到purePhoneNumber;
- 查数据库确认用户是否存在,不存在则创建用户,存在则更新最近登录时间;
- 返回自定义token给前端,后续所有请求都携带这个token。
这个流程里最容易出错的地方是session_key的有效期问题。jscode2session返回的session_key只有5分钟有效,前端如果先调wx.login拿code,过一会儿再走手机号授权,解密就会失败。我的解决办法是:把wx.login调用放到手机号授权按钮的点击事件里,保证code的获取和解密请求在几秒钟内完成,不跨越长时间的操作间隔。
前端代码大致长这样:
Page({ handleLogin(e) { wx.login({ success: async (res) => { const code = res.code; const encryptedData = e.detail.encryptedData; const iv = e.detail.iv; const res2 = await wx.request({ url: 'https://yourdomain.com/api/login', method: 'POST', data: { code, encryptedData, iv } }); if (res2.data.code === 0) { wx.setStorageSync('token', res2.data.data.token); wx.switchTab({ url: '/pages/index/index' }); } else { wx.showToast({ title: '登录失败', icon: 'none' }); } } }); } });还有一个小细节:手机号授权按钮只能触发一次,用户如果点了拒绝,再次点击按钮不会再弹出授权。处理办法是在拒绝后的界面上放一个"刷新授权"文本按钮,引导用户去小程序设置页重新打开手机号权限,否则这个用户永远无法完成登录。
3.2 顶部导航栏高度的适配问题
开发阶段我用了自定义导航栏,因为要嵌入老人端的大字号标题和返回逻辑,结果被顶部导航栏高度问题折磨了一整天。问题在于:不同机型的系统状态栏高度不同,顶部胶囊按钮(右上角的胶囊形状按钮)位置也不同。如果导航栏高度写死,iPhone 14 Pro Max和一台老款Android千元机上的显示效果会差很多。
微信官方提供了一个可靠的适配方案,核心是用wx.getMenuButtonBoundingClientRect获取胶囊按钮的位置信息,再结合系统信息计算导航栏高度:
const getNavBarInfo = () => { const menuButton = wx.getMenuButtonBoundingClientRect(); const systemInfo = wx.getSystemInfoSync(); // 胶囊按钮顶部到屏幕顶部的距离,就是状态栏高度+导航栏顶部留白 const statusBarHeight = systemInfo.statusBarHeight; // 导航栏高度 = (胶囊按钮顶部到屏幕顶部的距离 - 状态栏高度) * 2 + 胶囊按钮高度 const navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height; return { statusBarHeight, navBarHeight, navBarTop: menuButton.top }; };这个计算方案的核心逻辑是:胶囊按钮在导航栏里是垂直居中的,所以导航栏总高度等于"胶囊上方空隙的两倍+胶囊自身高度"。实测下来,在几十台不同机型上基本都能准确适配。开发自定义组件的时候,导航栏的paddingTop要绑定这个计算值,不要写固定像素。
3.3 面向老人的UI设计:字体、按钮、对比度、反馈
适老交互是这个项目区别于普通管理系统的核心。社区工作人员给老人演示系统时,最常出现的场景是:老人戴起老花镜,凑近屏幕,手指悬在半空不知道点哪里。针对这种情况,我做了几个硬性设计约定:
- 全局基础字号不低于17px,关键信息的字号在22px以上。小程序默认font-size是14px或16px,这个字号对老人来说太小了。我把页面style里统一设置了font-size: 17px,关键数值用28px以上加粗显示。
- 按钮的最小高度不低于44px。微信小程序官方建议的点击区域是44x44pt,但实践下来老人需要50px以上的高度才不容易误触。所有底部主操作按钮高度设为96rpx(48px),且整行可点击。
- 信息密度控制。一个页面只做一件事。健康数据录入页只放一个输入区域和确认按钮,不要堆叠多个表单项。老人的注意力非常单一,页面要素越多,误操作概率越高。
- 触觉和视觉反馈要明显。点击按钮后的toast提示文字要放大,加震动反馈(wx.vibrateShort),数据保存成功之后弹窗持续时间比常规多1秒。
3.4 健康数据录入手势与防误触
录入血压数据时,老人最容易输错的是收缩压和舒张压的位置。传统表单是"收缩压___mmHg,舒张压___mmHg"两个输入框,老人经常把两个数填反。我在页面上做了一个大卡片设计:上方一个超大的数字输入框,标签是"高压",下方是"低压",中间用一条粗线隔开,并且配上颜色区分——高压用暖色,低压用冷色。录入完成后有一段确认文案:"高压138,低压85,对吗?"需要点击"确认无误"才提交。这个确认步骤看起来多了一次操作,但实际大幅度降低了错录率。
另外,为了照顾手抖的老人,数字输入框用了type="number"并且最大长度限制为3位(血压值不可能超过三位数),同时禁用了键盘自动联想。这些细节单看都很小,但叠加起来就决定了这套系统老人到底用得起来还是被搁置。
4. Flask后端:健康档案、预约提醒与权限设计的API实践
4.1 核心API清单
后端API大概是这样的,全部返回统一的JSON结构:{code: 0, msg: "ok", data: {...}}。
| 接口路径 | 方法 | 功能 | 角色 |
|---|---|---|---|
| /api/login | POST | 微信登录,换取token | 全部 |
| /api/profile | GET/POST | 查看/更新健康档案 | 老人/家属 |
| /api/health-record | GET/POST | 提交或查询体征数据 | 老人/家属 |
| /api/health-record/trend | GET | 获取体征趋势数据 | 老人/家属/医生 |
| /api/reminder | GET/POST | 查询/创建用药提醒 | 老人/家属 |
| /api/reminder/checkin | POST | 用药打卡 | 老人/家属 |
| /api/appointment | GET/POST | 预约/查询随访 | 老人/家属/医生 |
| /api/doctor/abnormal-list | GET | 获取一周内异常体征老人列表 | 医生 |
| /api/admin/config | GET/POST | 读取/修改异常判定阈值 | 医生 |
4.2 体征数据的读写与异常判定逻辑
health_record表里只存最干净的原始数据,所有的业务判断都在读取时实时算。比如血糖值录入后,后端会同时返回一个status字段:normal(正常)、borderline(临界)、abnormal(异常),但不会把status写入数据库。这样做的原因是医疗阈值会调整,如果写死在历史记录里,未来阈值改了历史数据就没法重新判定。
异常判定逻辑放在services模块里做独立的函数,方便单元测试:
def judge_blood_glucose(value_mmol_l, config): """根据配置判定血糖状态,config从数据库读取""" if value_mmol_l <= config['normal_high']: return 'normal' elif value_mmol_l <= config['borderline_high']: return 'borderline' else: return 'abnormal'注意一点:体征数据单位是用户输入的原始单位,后端判定前要根据配置文件统一换算成标准单位。比如血糖有些家用血糖仪显示的是mmol/L,有些显示的是mg/dL,换算关系是mg/dL = mmol/L × 18。这个换算最好由后端统一完成,前端直接显示原始单位即可,否则两个端都得维护一套换算逻辑。
4.3 用药提醒与随访预约的定时任务
用药提醒不能只靠前端定时器,因为微信小程序一旦退出就被冻结,定时功能不可靠。我的方案是Flask后端加APScheduler做定时任务,每天凌晨扫描medication_reminder表,把当天需要提醒的老人和对应时间整理出来,调用微信的订阅消息接口下发提醒。
这里有一个关键的前置条件:小程序必须申请"订阅消息"模板,并且在用户操作时主动调用wx.requestSubscribeMessage让用户授权。授权是一次性的,用户每次同意只能接收一次提醒,想让老人每周都收到提醒,就得在每次录入或确认提醒时重新触发授权申请。很多开发者在这个环节想不通,以为订阅一次就能持续推送。实际业务里我用的策略是:每次老人或者家属主动使用系统(比如打卡记录用药)后,弹窗请求下一次的订阅授权,效果还可以。
APScheduler的配置很简单,用BackgroundScheduler加cron触发器:
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger scheduler = BackgroundScheduler(timezone="Asia/Shanghai") scheduler.add_job( send_medication_reminders, trigger=CronTrigger(hour=6, minute=30), id="daily_reminder", replace_existing=True, ) scheduler.start()4.4 数据权限与隐私保护
健康医疗数据属于敏感个人信息,权限设计上绝对不能图省事。这个系统的角色有三类:老人、家属、医生。权限规则如下:
- 老人只能查看和编辑自己的档案、体征数据、用药记录;
- 家属只能查看和编辑自己绑定的老人的数据,绑定关系在系统中需要老人本人或社区医生审核,防止随意查看他人健康信息;
- 社区医生可以查看本辖区内所有老人的数据,但医生的操作需要留痕,所有查询行为都记录access_log表,包含医生id、老人id、查询时间和操作类型。
接口鉴权用token方案实现,所有API请求的header必须携带Authorization: Bearer 。Flask后端写了一个装饰器统一处理:
def login_required(role=None): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): token = request.headers.get('Authorization', '').replace('Bearer ', '') user = verify_token(token) if not user: return jsonify({"code": 401, "msg": "未登录或登录已过期"}), 401 if role and user.role != role: return jsonify({"code": 403, "msg": "无权限访问"}), 403 g.user = user return func(*args, **kwargs) return wrapper return decorator还有一个容易忽略的细节:接口返回老人列表给医生端时,默认只能看到姓名和联系方式,具体体征值必须点击详情才能查看,且详情查看会被记录到access_log。这是为了让医生"有事可查、有迹可循",同时也符合最小必要原则。
5. 从开发机到服务器:Flask部署与小程序上线的完整链路
5.1 环境准备与依赖管理
项目开发环境用的Python 3.8 + Flask 2.2 + MySQL 8.0。部署时最忌讳直接在服务器上装一堆全局依赖包,一定要用虚拟环境隔离。我的做法是在项目根目录下创建venv:
python3 -m venv venv source venv/bin/activate pip install -r requirements.txtrequirements.txt的生成别在开发机上乱搞,用pip freeze的方式会把无关包也打进去。我在项目里手动维护这个文件,只列关键依赖,并且固定版本号:
flask==2.2.5 flask-sqlalchemy==3.0.5 flask-cors==4.0.0 pymysql==1.0.2 cryptography==41.0.3 requests==2.31.0 apscheduler==3.10.4 gunicorn==21.2.05.2 WSGI服务器选择与配置
Flask自带的开发服务器(app.run)性能极差,绝对不能用于生产环境。我在生产环境用的是gunicorn + 多worker模式。启动命令是:
gunicorn -w 4 -b 0.0.0.0:5000 run:app四个worker对这个量级的并发足够。如果服务器内存只有2G,建议只开2个worker,因为每个worker的MySQL连接池和Flask上下文都会吃内存。另外别忘了在gunicorn前面加一层Nginx做反向代理,Nginx负责静态文件、HTTPS终结、负载平衡到gunicorn的socket。
Nginx配置的关键地方是转发放大CDN和大文件上传的body大小限制,小程序上传体检报告图片时需要调整client_max_body_size参数,默认的1m根本不够用:
server { listen 443 ssl; server_name yourdomain.com; client_max_body_size 20m; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }5.3 HTTPS证书与域名备案
微信小程序的环境要求非常硬性:所有请求的接口必须是HTTPS,而且域名必须在小程序后台的request合法域名里配置。这个环节有两点需要格外注意:
- 域名必须备案。服务器可以买香港或海外节点不备案,但小程序的request域名必须是已备案的域名,否则小程序访问接口时会直接报"不在以下合法域名列表中"。
- SSL证书用免费的就行(比如Let's Encrypt),但要注意证书有效期三个月,需要配置自动续期脚本。我遇到过不止一次因为证书过期导致用户突然访问不了的情况,排查方式就是打开小程序看到报错信息和网页端完全不一样,非常干扰定位。
5.4 小程序打包上传与提审
小程序开发完成提交审核之前,有一些容易被忽略的检测项。我整理一个检查清单:
- 隐私保护指引必须配置。2023年后微信强制要求小程序声明收集用户信息类型,如果用了手机号登录、位置、相册权限,必须在后台填写隐私保护指引,否则提审会被驳回。
- wx.request的url不能是IP地址或localhost,必须是HTTPS域名。
- 不要在小程序代码里硬编码任何密钥。我见过有人把后端的secret key写在小程序端JS里,这是非常糟糕的习惯,小程序代码可以被人反编译。
- 提审时常用的功能都要实际可点,不能有mock数据页面。微信审核员会真的去点,如果点了之后转圈报错,驳回理由会写"核心功能不可用"。
- 个人主体小程序不能开通支付、不能获取手机号、部分类目无法选择。如果项目需要这些能力,提前注册企业主体或个体户主体。
6. 上线之后的真实反馈与迭代方向
6.1 实际使用中的意外情况
系统上线两个月后,我做了个小范围的用户回访,得到几个颠覆性的反馈。首先是老人端活跃度远低于预期,但家属端的操作量非常高——大部分老人的数据实际上是子女帮忙录的。原以为适老交互做好了老人就会自己用,实际上很多老人连"点开小程序"这个动作都要子女代劳。这说明适老设计的方向没有错,但对使用者的判断有偏差:应该把家属代操作流程做得更顺滑,而不是把全部精力押在老人自主操作上。
第二个意外是医生端对"异常数值列表"这个功能的使用频率极高,但对"趋势图"的反馈很平淡。医生想要的是"哪些老人这周需要重点关注",而不是"某个老人过去的90天曲线"。趋势图对医生来说是诊断参考,但对社区随访安排而言,优先级排序列表才是真正的决策工具。后面我花了更多精力把异常列表做精细:支持按病种筛选、按风险等级排序、支持批量生成随访任务,这个功能比任何花哨的可视化都实用。
第三个问题是"用药打卡"很快沦为形式主义。老人或家属打了卡并不代表真的吃了药,但社区工作人员也没有精力一一核实。我后来在系统里增加了"代打卡"标记字段:家属代打卡的记录会有标识,社区医生随访时重点核实这类记录的真实性。系统设计不能理想化地假设所有数据都是真实录入的,要给自己留出寻找真相的余地。
6.2 后续迭代的几个方向
系统落地后的迭代计划主要围绕三个方面:
第一是硬件对接。通过与蓝牙血压计、血糖仪对接,让数据直接通过蓝牙传进小程序,省去手动录入环节。这个方向技术上不复杂,难点在于设备兼容性和老人设备的普及率。
第二是语音交互。很多老人打字困难,但说话没问题。小程序端的语音转文字或者直接语音指令"报告血压"可以作为录入入口,现在各家云服务商的语音识别API已经比较成熟,这块开发成本主要在对老人方言的支持上。
第三是跨端扩展。如果后续有机构端后台或大屏展示需求,可以考虑用Flutter或uniapp把现有小程序页面复用过去。不过以我这次项目的经验来看,短期内没有这个必要——微信小程序的生态能力已经覆盖了社区健康服务的全部场景,与其急着跨端,不如先把核心数据质量和医生工作流打磨扎实。
做这种社区级的健康服务系统,周期长、细节多、收益看起来也不像互联网产品那样性感,但每次看到社区医生通过系统提前发现血压异常的老人并上门干预,还是挺有成就感的。如果说这个项目有什么最值得记住的经验,那就是:技术永远只是工具,真正的产品核心是对老年人生活状态的理解和对服务流程的尊重。功能可以上线后再迭代,但对使用者的敬畏必须在第一天就建立。