资讯动态

Android+小程序构建社区医疗健康管理系统:中医体质测评与随访实战

发布时间:2026/9/10 8:48:48 来源:尧图企业网站定制
去年我在做一套社区医疗健康管理项目的时候碰到了一个特别典型的场景社区卫生服务中心的家庭医生要管理上千名签约居民其中一大半是上了年纪的老人。他们不会装App、记不住账号密码但几乎人人都会用微信每天在家庭群里转发养生文章。而医生这边白天要下乡随访、量血压、测体脂、填健康档案晚上还要把纸质表格一条条录入电脑手头的安卓设备也五花八门。这个项目最后做成了“基于Android的中医体质社区医疗居民健康问诊管理系统”居民端是微信小程序医生端是Android原生App后端统一走Spring Boot服务。居民打开小程序就能做中医体质测评、在线问诊、查健康档案医生用Android端扫码绑定居民、拉取中医体质测评数据、记录随访结果、接收异常预警。这套系统跑通之后原来一天才能整理完的随访数据现在小区里当场就能录完居民的体质报告和调养建议也会自动推送到小程序上。本文就把这套系统从设计到落地的完整过程拆开来讲包括为什么选“Android 小程序”双端、中医体质测评的算法怎么落到代码里、两端登录态怎么打通、以及我在真机调试和上线阶段踩过的那些坑。适合正在做医疗类小程序、跨端健康管理系统的开发者参考也适合想了解中医信息化怎么落地的产品经理和全栈工程师。1. 整体设计与技术选型为什么不是纯App也不是纯小程序1.1 需求背景与用户画像社区医疗和医院门诊有个本质区别医生面对的是“签约居民”不是“患者”。健康管理是常态动作得经常随访、定期测评、持续跟踪。所以系统的核心不是一次性的挂号问诊而是长期积累的健康档案管理。居民画像很鲜明40岁以上占大头尤其60岁以上老人手机操作能力有限。他们对“下载一个App”这件事有天然抵触但微信用得比谁都溜。社区医生则是40岁以下的基层医疗人员平时工作中已经习惯用安卓设备对原生App的扫码、蓝牙、拍照、离线存储这些硬能力有实际需求。所以第一版方案讨论时有人提议干脆全做小程序医生也用小程序。后来被否了原因很现实医生端要连蓝牙体脂秤、血压计要批量拍照存档要离线录入随访记录这些操作在小程序里体验很别扭尤其是蓝牙重连和大量照片上传小程序的后台保活能力撑不住。而居民端如果做成App光一个“让老人完成注册登录”就能劝退一半用户。1.2 双端定位与架构设计最终确定的架构很清晰居民端微信小程序。功能集中在中医体质测评、健康档案查看、在线问诊、预约随访、调养建议推送。医生端Android原生AppAndroid Studio开发。功能集中在居民管理、扫码绑定、体质测评结果列表、随访记录采集、蓝牙设备对接、异常预警。后端Spring Boot MySQL Redis提供统一REST API鉴权用JWT居民端和医生端都通过Token访问。这个结构的好处是“一个业务核心两个触达入口”。后端只管业务数据不管端上体验。居民端追求简单易用医生端追求稳定高效各取所长。数据模型上居民的基本信息以微信号UnionID为唯一标识医生端通过扫码或者手机号绑定居民后两端看到的是同一份数据。1.3 数据模型核心表设计医疗数据的核心是“档案-测评-随访”三层结构。我直接列出这几张核心表的关键字段方便后面理解功能实现居民健康档案居民ID、姓名、性别、出生日期、手机号、微信号UnionID、住址、签约医生ID。体质测评记录测评ID、居民ID、测评时间、九种体质转化分、判定结果、调养方案、测评状态。问诊记录问诊ID、居民ID、问诊时间、症状描述、医生回复、处理状态。随访记录随访ID、居民ID、随访日期、血压、心率、体脂率、体重、随访医生ID、随访备注。设计时有个容易忽略的地方体质测评结果不是算一次就完了居民每隔三个月需要复测前后数据要对比才能判断调养方案是否有效。所以测评记录表里必须保留每次的“九种体质转化分”而不是只存一个最终判定结果。2. 核心功能拆解中医体质辨识、问诊流程、医生工作台2.1 中医体质测评问卷九种体质的判定逻辑中医体质辨识是整个系统最核心的业务模块。标准参考中华中医药学会发布的《中医体质分类与判定》将体质分为九种平和质、气虚质、阳虚质、阴虚质、痰湿质、湿热质、血瘀质、气郁质、特禀质。测评问卷采用标准化量表一共60道题每个体质类型对应7道左右题目居民根据自己的真实感受打分。但社区场景下60道题对老人来说太长很多人填到一半就放弃了。实际项目里我做了精简版每个体质类型选取4道高区分度题目一共33题含平和质每道题按五级计分没有1分、很少2分、有时3分、经常4分、总是5分。判定公式如下原始分 各条目分值相加转化分 原始分 - 条目数 / 条目数 × 4 × 100判定标准转化分 ≥ 60分判定为该体质30分 ≤ 转化分 60分判定为倾向该体质转化分 30分判定为否。这里有个经验之谈刚开始用60分作为唯一判定线实测发现很多人同时有“倾向”表现全部罗列出来会乱。后来调整了规则先判定异常体质平和质必须满足“所有偏颇体质转化分均 40分”才成立否则取转化分最高的前三种体质作为主要结果并在报告中体现“倾向”提示。2.2 在线问诊流程决策树 分级预警问诊模块没有做成开放聊天室而是做了“结构化问诊 医生回复”的轻问诊模式。居民选择症状分类填写主诉和持续时间系统根据内置的疾病知识库推荐对应科室并生成预警级别。这块底层逻辑是用决策树实现的。例如居民选择“胸闷、胸痛”知识库规则引擎会继续追问疼痛是否向左肩放射是否伴随大汗如果“是”则标记高危预警直接建议拨打急救电话如果“否”则推荐心内科随访医生端同步收到问诊工单。分级预警的规则我用一个简单的权重表来配置症状风险等级权重、持续时间权重、基础病史权重三部分叠加超过阈值就触发预警。例如血压值 180/110mmHg直接触发高危血糖 16.7mmol/L且伴随意识模糊触发高危。这些规则全部放在后端前端只负责展示结果方便后续医疗知识库调整而不用发版。2.3 医生端Android工作台模块Android端主要面向医生功能模块包括今日工作台显示待处理问诊、待随访居民、异常预警列表。居民管理扫码绑定居民、按网格/楼栋筛选居民、查看居民健康档案。体质测评详情拉取居民历次体质测评结果可视化展示转化分趋势。随访采集手动录入或蓝牙连接体脂秤/血压计自动采集数据。离线缓存社区医院网络环境不稳定随访数据先存本地数据库网络恢复后自动同步。这里用的技术栈是Kotlin Jetpack MVVM架构网络层用Retrofit OkHttp本地缓存用Room蓝牙用系统BluetoothGatt封装。架构上没做得很重但MVVM的分层对于后续维护是真的有帮助——UI层、ViewModel层、Repository层各司其职后面加新功能不会牵一发动全身。3. 实操过程从登录打通到体质测评算法落地3.1 小程序登录态与微信用户信息获取小程序端第一个难点就是登录态。居民打开小程序要先完成微信授权拿到UnionID然后和健康档案绑定。这里说的“获取登录后的微信用户失败:wx1cb4398e1413dce7”这类报错大概率是混淆了wx.login、wx.getUserProfile、wx.getUserInfo这三个接口的职责。正确的流程是// 1. wx.login 获取临时 code wx.login({ success: async (res) { const code res.code; // 2. 将 code 发送到后端后端调用 code2Session 换取 openid 和 session_key const response await wxRequest(/api/auth/code2session, { code }); // 3. 后端返回自定义登录态 token const token response.data.token; wx.setStorageSync(token, token); } });注意wx.login 自2021年起不再静默返回用户信息。用户头像和昵称需要通过“头像昵称填写能力”实现也就是用户主动点击填写的按钮或者用新版open-type选择组件button open-typechooseAvatar bindchooseavataronChooseAvatar选择头像/button input typenickname placeholder请输入昵称 /这个改动对我这个项目影响很大。最初版本是用户一进来就弹窗授权很多老年人直接关掉了导致档案绑定率很低。改成“先使用、后完善”的策略后——不强制用户一开始就填头像昵称而是用默认图标顶替等签约家庭医生的时候再引导完善——绑定率才上来。另一个坑是code2Session的code有效期只有5分钟且只能使用一次。有段时间我发现重复登录时偶尔会报错排查下来是前端并发请求导两个接口同时用了同一个code。解决办法是初始化登录态时做一个Promise合并只允许一个code2session请求在途。3.2 体质辨识算法落地Kotlin实现转化分计算后端和Android端都要用到体质判定逻辑。后端在测评提交时计算并存储Android端在离线模式下也要能本地计算。所以我把转换分计算逻辑在各端各写了一份。Android端用Kotlin实现如下object ConstitutionEvaluator { // 九种体质类别 enum class ConstitutionType(val code: String) { PING_HE(平和质), QI_XU(气虚质), YANG_XU(阳虚质), YIN_XU(阴虚质), TAN_SHI(痰湿质), SHI_RE(湿热质), XUE_YU(血瘀质), QI_YU(气郁质), TE_BING(特禀质) } /** * 计算转化分 * param rawScore 条目原始分之和 * param itemCount 条目数量 */ fun calculateConvertedScore(rawScore: Int, itemCount: Int): Double { if (itemCount 0) return 0.0 return (rawScore - itemCount).toDouble() / (itemCount * 4).toDouble() * 100 } /** * 判定体质 * param scores 各体质转化分映射 */ fun evaluate(scores: MapConstitutionType, Double): ListConstitutionResult { // 先判断平和质 val hasPianpo scores.entries .filter { it.key ! ConstitutionType.PING_HE } .any { it.value 40.0 } val isPingHe if (!hasPianpo) { scores[ConstitutionType.PING_HE]?.let { it 60.0 } ?: false } else { false } if (isPingHe) { return listOf(ConstitutionResult(ConstitutionType.PING_HE, 平和质, is)) } // 取偏颇体质中转化分最高的三个分类别 return scores.entries .filter { it.key ! ConstitutionType.PING_HE } .sortedByDescending { it.value } .take(3) .map { entry - val level when { entry.value 60.0 - is entry.value 30.0 - 倾向 else - 否 } ConstitutionResult(entry.key, entry.key.code, level) } } }这里有个很容易踩的坑计算转化分时原始分要用“该体质所有条目分值之和”不是随便挑几道题算完直接平均。而且如果量表做了精简不能直接把精简后的条目套进标准公式——标准公式的条目数必须和实际使用的条目数一致否则转化分会偏低或偏高。我们最初用了标准公式但只取了4题算出来的分数整体偏低后来调整了条目数参数才正确。3.3 Android端与小程序端数据同步Token设计与绑定关系两端数据同步的核心问题是怎么让医生在小程序里看到的居民和在Android端绑定的居民是同一批数据我的做法是整段链路以“签约关系”为中间表。小程序端居民登录后先创建/关联健康档案用UnionID然后通过“预约随访”功能提交自己的网格归属。医生在Android端通过扫描居民小程序里的二维码将居民档案与医生账号建立绑定关系。二维码内容是一个加密的绑定串格式是health://bind?uidxxxts1700000000000signmd5(uidtssecret)Android端扫码后解析认证调用后端绑定接口成功后两端数据实时打通。Token设计上小程序和Android端各自持有JWT其中包含userType字段区分居民和医生。后端根据userType校验接口权限例如居民端不能访问医生工作台接口。Android端离线缓存是另一个重点。Room数据库保存了居民基本信息、最近一次体质测评结果、当天的随访记录。断网时新增的随访记录先标记为“待同步”网络恢复后通过WorkManager的约束条件自动上传。实测中这个机制在社区医院地下诊室特别实用——那边信号经常只有一格。3.4 蓝牙设备接入体脂秤和血压计的数据读取随访场景里医生经常要测体脂和血压。传统做法是手动记录又慢又容易错。后来我直接对接了蓝牙体脂秤和血压计Android端通过BLE协议读取测量数据自动填充到随访表单。蓝牙接入的核心流程// 1. 扫描设备 private fun startScan() { val scanner bluetoothLeScanner val settings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() scanner.startScan(listOf(ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(0000180d-0000-1000-8000-00805f9b34fb)) .build()), settings, scanCallback) } // 2. 连接并发现服务 override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { if (newState BluetoothGatt.STATE_CONNECTED) { gatt?.discoverServices() } } // 3. 读取体征数据特征值 private fun readMeasurement(gatt: BluetoothGatt) { val service gatt.getService(UUID.fromString(0000180d-0000-1000-8000-00805f9b34fb)) val characteristic service.getCharacteristic(UUID.fromString(00002a37-0000-1000-8000-00805f9b34fb)) gatt.readCharacteristic(characteristic) }Android 12以上必须动态申请BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限Android 14上还要关注“附近设备”权限细分。我之前在一个旧的安卓14测试机上一直连接失败最后发现是没适配附近设备权限的运行时请求逻辑加上BLUETOOTH_SCAN权限要配合“精确位置”一起声明改了AndroidManifest和运行时权限请求顺序后问题解决。3.5 开发环境版本选型Android Studio Hedgehog与AGP兼容性Android端开发环境这方面我也踩了版本兼容的坑。项目初期我用的Android Studio Hedgehog2023.1.1 Patch 2计划配套的AGPAndroid Gradle Plugin版本要仔细核对。Hedgehog版本自带的AGP最高支持到8.2.x如果强行用AGP 8.3Gradle同步会报错说插件版本不兼容。我的build.gradle配置如下// project-level build.gradle buildscript { repositories { google() mavenCentral() } dependencies { classpath com.android.tools.build:gradle:8.2.2 classpath org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22 } }AGP 8.x默认开启namespace配置旧的package配置要迁移到namespace这是从AGP 8.0开始的一个破坏性变更。如果是从老项目升级上来的AndroidManifest.xml里的package属性必须删掉否则编译直接报错。另外targetSdk如果设置为34Android 14微信小程序端某些回调行为也会有变化后面在常见问题里再细讲。4. 小程序性能优化与端上适配实战4.1 分包加载与分包异步化居民端小程序从一开始就做了分包因为整个系统除了问诊还有健康资讯、调养食谱、视频课程、签约协议等模块。主包只保留首页、登录、体质测评入口、个人中心这几个核心页面其他模块全部拆进分包。微信小程序主包限制是2MB整个项目超过5MB不分包根本过不了审核。我的分包方案是主包首页、登录、个人中心、体质测评分包A问诊、医生列表、预约记录分包B健康档案、随访记录、报告详情分包C健康资讯、调养食谱、视频课程分包的启动逻辑是用wx.navigateTo跳转分包页面时带上包路径例如wx.navigateTo({ url: /packageA/pages/inquiry/index });分包异步化是个新特性需要基础库2.20.1以上代码分包里可以通过require.async按需加载// 在分包A的页面中异步引用分包B的工具函数 async function loadReportTool() { const tool await require.async(../../packageB/utils/report-helper.js); return tool.generateReport(); }这个功能最大的价值是把真正的同步逻辑代码从首页启动路径中剥离减少首屏拉取体积。我们实测下来分包异步化改造后小程序的冷启动时间从4秒多降到2秒以内体感非常明显。4.2 顶部导航栏高度与底部安全区适配这类适配在开发社区里因为搜索词高频出现说明是大家普遍会遇到的。小程序端自定义顶部导航栏时不能硬编码状态栏高度和胶囊按钮位置必须通过系统API动态获取。// 获取胶囊按钮布局信息 const menuButton wx.getMenuButtonBoundingClientRect(); // 获取状态栏高度 const statusBarHeight wx.getSystemInfoSync().statusBarHeight; // 计算导航栏高度 const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height;底部安全区适配主要针对iPhone的刘海屏和底部小黑条。苹果手机底部有约34px的home indicator区域如果页面底部有提交按钮或tab栏不做适配就会被挡住。标准做法是使用env()函数.page-footer { padding-bottom: calc(12px env(safe-area-inset-bottom)); }如果是在普通页面底部还要配合constant()做旧版本兼容。这里有个实用小技巧直接把safe-area-inset-bottom的值通过WXS传给JS用于计算滚动位置可以避免在一些安卓WebView里env()不生效导致的高度异常问题。4.3 H5与小程序之间的定位互通项目里居民端的“附近药店”模块用到了web-view加载H5页面H5页面需要获取用户当前经纬度来展示附近的社区卫生服务中心。这里有一个很典型的坑H5直接调用wx.getLocation是无效的因为web-view环境不完全等同于小程序原生环境。正确做法是H5通过WeixinJSBridge获取定位或者干脆在小程序原生页面里调用wx.getLocation拿到经纬度再通过URL参数传入web-view// 小程序页面 Page({ onLoad() { wx.getLocation({ type: gcj02, success: (res) { const lat res.latitude; const lng res.longitude; this.setData({ webUrl: /pages/webview/index?url${encodeURIComponent(https://xxx.com/nearby)}lat${lat}lng${lng} }); } }); } });反过来H5内部如果有操作需要通知小程序刷新数据可以用wx.miniProgram.postMessage向小程序发消息但必须注意这个API只在用户点击行为触发时才可靠onUnload时postMessage不生效。小程序端通过bindmessage事件接收。4.4 小程序无法打开公众号文章的配置排查项目里有些科普文章是发在公众号上的小程序内部通过web-view或直接跳转打开。很多人卡在“小程序无法打开公众号文章”这个问题上大多数情况下不是代码问题而是配置没做全。在微信公众平台需要做两件事小程序与公众号必须关联同一主体或经过认证的关联主体。如果是web-view加载需要在“开发管理-开发设置-业务域名”里配置公众号文章所在域名的业务白名单。这里有个特殊点业务域名校验文件需要下载后放到该域名的根目录下但公众号文章链接本身就在mp.weixin.qq.com域下这个域名不能直接配到业务域名里。正确做法是如果是自己的公众号文章用小程序内部的web-view打开mp.weixin.qq.com域名时需要确认该公众号与小程序为同主体且已互相关联同时把这个链接作为临时链接做跳转。如果无法配置更稳妥的方式是直接用wx.openOfficialArticle或者通过复制链接到浏览器打开。这块业务逻辑配起来相对繁琐我在开发环境里用开发者工具的“不校验合法域名”开关能正常打开上线后却打不开就是这个原因。检查清单依次是关联关系、业务域名、校验文件、正式环境开关。5. 高频问题实录我从这个项目里总结的踩坑清单5.1 微信小程序登录态问题wx1cb4398e1413dce7之类错误开发微信小程序时“小程序获取登录后的微信用户失败”是一类很笼统的报错。我遇到的几个具体场景AppID在开发者工具里填了一个测试号真机预览时又换成了正式号但项目里的request域名还是测试环境的登录后接口返回404。使用code2Session时后端没有正确配置小程序AppSecret或者AppSecret填的是旧版本已重置的密钥。code已经在前一个请求里被消费后一个请求再拿同样的code去换openid微信服务器会报invalid code。排查顺序建议先在开发者工具Network面板看wx.login回调里有没有code再把code拿到后端单独调试看code2Session返回什么最后确认request域名是否在小程序后台配置了合法域名。大部分登录问题都是这三步能定位的。5.2 小程序抓包调试的注意事项联调阶段肯定要抓包看接口。小程序端的抓包工具我用的是Charles整体思路是手机和电脑连同一局域网手机设置HTTP代理指向电脑IP端口8888然后Charles上装好SSL Proxying证书。需要注意小程序正式环境的request必须走HTTPS证书校验失败的概率比较高要在手机系统里信任Charles证书并且在小程序后台把调试域名开起来。还有个坑微信开发者工具打开的页面如果用了“真机调试”抓包工具看不到接口流量因为流量走的是微信自带的调试通道。所以抓包一定要用“预览”模式在真机上操作或者在开发者工具的Network面板里直接看。我后来统一让测试人员用预览二维码加Charles抓包问题定位效率提升了一个档次。5.3 Android 14与蓝牙连接扫描不到体脂秤项目迭代到后期有台旧安卓手机升级到Android 14后BLE扫描一直找不到体脂秤。排查下来是Android 14对蓝牙权限的管控更严格了。除了常规的BLUETOOTH_SCAN、BLUETOOTH_CONNECT权限Android 12还需要申请“附近设备”权限Android 14上这个权限的申请弹窗时机和次数都变了。另外Android 14上如果targetSdkVersion升级到34还需要在AndroidManifest中显式声明uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT /注意usesPermissionFlagsneverForLocation必须在蓝牙扫描不用于位置定位时才加加了之后系统不会触发定位权限关联请求但同时如果设备固件上报了位置信息系统会过滤掉。体脂秤一般不上报位置所以可以安全声明。如果扫描到设备但连不上通常是BLE连接并发冲突同一时间只能有一个Gatt连接在跑多个设备轮询时要做好串行控制。5.4 顶部导航栏与安全区的常见兼容问题这里再补充一个常见问题汇总因为我发现搜索“小程序头部标题”“微信小程序顶部导航栏高度”的人特别多。很多效果做不出来核心原因是不同手机的胶囊按钮尺寸不一致。我用过最高效的方案是封装一个自定义导航组件每次进入页面时动态计算导航栏高度并利用CSS变量将高度值注入到页面样式里这样组件内部和页面内部都能统一使用。示例代码.nav-bar { height: var(--nav-bar-height); padding-top: var(--status-bar-height); } .page-container { min-height: 100vh; padding-bottom: calc(20px env(safe-area-inset-bottom)); }5.5 微信开发者工具与编码问题我的项目里用到了大量中医术语和特殊符号比如“炁”“郁”这类字在小程序开发者工具里偶尔出现乱码。排查后发现是项目编码不是UTF-8。微信开发者工具默认按UTF-8读取源码如果你在Windows下用记事本编辑过文件且存成了GBK就会出现乱码。解决办法是把所有源文件统一保存为UTF-8 without BOM并且在编辑器里设置默认字符集为UTF-8。另一个常见问题是Android Studio里打开的Java/Kotlin文件如果也是中文注释乱码同样要检查File Encoding设置。我习惯在项目根目录的gradle.properties里加一行file.encodingUTF-8避免在不同机器上拉代码后编码不一致。5.6 Codex辅助开发微信小程序的尝试开发过程中我试用过用AI辅助生成部分前端页面主要是把后端返回的数据渲染到列表上。减少了不少琐碎工作但注意它生成的小程序API调用经常混入网页端API或者生成不存在的组件属性所以在整体应用前还是要过一遍逻辑。对于表单校验、列表分页这类逻辑相对固定的页面AI辅助的效率确实高但涉及医疗规则、体质判定这类强业务逻辑的地方我还是坚持手写和人工review性能和正确性都更可控。6. 一点个人体会这套系统从原型到上线差不多花了四个月中间反复调整的地方很多但回过头看真正决定项目成败的不是技术栈选得多新而是对社区医疗场景的理解够不够深。居民端小程序为什么必须先做体质测评、后做在线问诊因为对大多数老人来说问诊是有病才去的事而体质测评是自我认知的入口做完之后能看到一份属于自己的调养方案这种获得感驱动他们把测评分享给家人也驱动他们持续使用小程序。医生端为什么必须离线可用因为社区随访的真实环境不是医院诊室而是在活动室、楼道、甚至老年人家里的客厅。再分享一个小技巧中医体质测评的结果不要只给一个“你是痰湿质”的结论一定要同时给出可执行的调养建议包括饮食宜忌、穴位按摩、作息建议最好还能关联到对应的健康资讯文章和食谱。这个设计极大提升了用户粘性也让家庭医生在签约回访时有话可聊。后续我还在考虑几个扩展方向一是把体质测评结果和可穿戴设备的睡眠、心率数据做关联分析二是在Android端加入更多体征蓝牙设备支持三是把问诊知识库逐步升级为基于决策引擎的自动分诊系统降低家庭医生的工作负担。这个方向还在持续迭代到时候有新的踩坑经验再来分享。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价