做这个项目之前,我一直觉得中医体质辨识就是个问卷打分的事,填完表出个报告就算完成。但真把“基于Android的中医体质社区医疗居民健康问诊管理系统”从需求文档一步步落到能跑的小程序和Android端,才发现这个题目覆盖的范围比想象中大得多。小程序端要解决居民填问卷、看报告的体验,Android端要承载医生问诊工作台,后端还得把体质判定、健康档案、随访记录串成一条完整的业务链。这篇文章就围绕这套系统,把我从选型、建表、写判定算法到上线排查的完整过程梳理一遍,给准备做同类医疗健康方向项目的朋友一个可直接参考的版本。
整套系统面向的典型场景是社区医疗:居民在微信小程序里完成中医体质测评、维护健康档案、发起问诊;社区医生用Android平板或手机登录工作台,查看居民档案、处理问诊请求、开个体化干预方案。听起来不复杂,但真正做起来,从业务建模到技术坑,随便一个环节都能让人折腾好几天。
1. 项目整体设计与业务逻辑拆解
1.1 社区中医健康管理到底在管理什么
做任何系统之前,先把业务闭环想清楚。社区中医健康管理最核心的逻辑不是“看病”,而是“未病先防”和“慢病调理”。居民不可能每天往社区医院跑,医生也没有精力一对一盯着每个人,所以系统本质上要解决三件事:体质辨识、干预指导、随访跟踪。
这个链条对应到系统功能上就是三个模块。第一,居民端小程序提供标准化的中医体质测评,完成九种体质判定。第二,医生端根据体质结果结合居民的起居、饮食、既往史,给出个体化的调养建议。第三,系统自动生成随访计划,到时间提醒居民复测体质,让医生能看到干预前后的数据变化。这就是一个完整的“辨识-干预-复测”闭环。
我见过不少同类项目,只做了问卷和报告展示,医生端就是个摆设,随访也没有任何数据支撑。这种系统上线后基本没人用。所以设计阶段我花了大量时间梳理角色和流程,确认居民、医生、系统管理员三类角色的边界,再开始画页面和建表。
1.2 九种体质怎么变成可计算的业务规则
中医体质分类依据的是中华中医药学会发布的《中医体质分类与判定》标准,把人群分为九种体质:平和质、气虚质、阳虚质、阴虚质、痰湿质、湿热质、血瘀质、气郁质、特禀质。每一种体质对应一组条目(题目),受试者根据近一年的感受给每个条目打1到5分。
判定规则在产品层面必须写得非常明确,不然开发阶段天天扯皮。核心算法是这样:每个体质条目得分相加得到原始分,再通过公式换算成转化分。转化分的计算公式是:转化分 =(原始分 - 条目数)/(条目数 × 4)× 100。比如阳虚质有7个条目,7题得分加起来是21,转化分就是(21-7)/(7×4)×100 = 50分。
判定标准也分情况。平和质要求转化分大于等于60分,同时其他八种偏颇体质的转化分都小于30分,才算真正的平和质。偏颇体质则看转化分:大于等于40分判定为“是”,30到39分判定为“倾向”,小于30分判定为“否”。这些阈值不能自己拍脑袋改,判定结果直接决定医生的干预方案,边界值搞错,整个报告的逻辑就跑偏了。
1.3 双端一后的架构为什么这么分
标题里写的是“基于Android”和“小程序”,所以一开始就很明确:居民入口用微信小程序,医生和管理端用Android原生。很多人问为什么不用两个小程序省事,原因很简单,角色差异太大。居民端是典型的C端工具,低频次、轻操作、用完即走,小程序天然适合。医生端则更像一个生产工具,每天要处理问诊列表、写记录、看趋势图、可能还要连蓝牙设备读血压计体脂秤的数据,这种场景下原生Android的稳定性和外设兼容性要比小程序好得多。
后端我选了Spring Boot,提供统一的RESTful API。小程序和Android端都不直接访问数据库,而是通过后端接口完成业务。这样后期如果想把医生端改成iOS版,或者再扩展一个管理后台,只需要复用同一套接口就行,前端随便换。
2. 技术选型解析与开发环境搭建
2.1 技术栈全景与选型对比
先交代一下最终选型:后端是Spring Boot 2.7 + MyBatis-Plus + MySQL 8.0 + Redis,部署在云服务器上。小程序端最开始纠结过原生还是uni-app,后来选了微信小程序原生开发。Android端用Kotlin + Jetpack组件,网络层Retrofit + OkHttp,数据库用Room。这里把几个关键选型对比写出来。
| 技术决策点 | 备选方案 | 我的选择 | 理由 |
|---|---|---|---|
| 小程序框架 | 微信原生 / uni-app / Taro | 微信原生 | 项目不要求多端复用,原生调试最直接,性能也好 |
| Android语言 | Java / Kotlin | Kotlin | 官方推荐,协程写异步逻辑比回调舒服太多 |
| Android架构 | MVC / MVP / MVVM | MVVM | ViewModel + LiveData做数据驱动,页面状态清晰 |
| 后端框架 | Spring Boot / 若依脚手架 | Spring Boot + 若依改 | 若依自带权限和代码生成,能省下大量管理后台开发时间 |
| 数据库 | MySQL / PostgreSQL | MySQL | 资料多、团队熟,社区场景并发量不大,完全够用 |
这里多说一句,很多毕业设计和中小项目一上来就堆技术栈,好像不够新就不专业。其实对社区医疗这种业务逻辑重于技术规模的场景,稳定、熟悉、方便排查才是最关键的。项目后期光排查微信登录和蓝牙权限问题就花了不少时间,如果技术栈再冷门一点,遇到问题连资料都搜不到,会很痛苦。
2.2 微信小程序侧的冷启动配置
小程序端的坑从注册那一刻就开始了。个人主体的小程序很多能力受限,比如不支持web-view组件,后面说到的“无法打开公众号文章”就跟主体类型有关,所以建议直接用企业主体注册。注册完拿到AppID,在微信开发者工具里导入项目,AppID填进去。很多登录报错(包括标题里出现过的那种wx1cb4398e1413dce7格式的AppID相关报错)基本都是AppID配置错误或后台Server域名没配置导致的。
开发者工具建议用最新稳定版,基础库版本在project.config.json里指定。注意真机调试和模拟器表现差异很大,涉及wx.login、蓝牙、定位的功能,必须真机验证,模拟器只能看UI布局。
还有一个容易忽略的点是“小程序后台-开发管理-开发设置-服务器域名”。如果request请求的接口不是HTTPS,或者域名没有在小程序后台配置为request合法域名,真机上所有网络请求都会失败。开发阶段可以勾选“不校验合法域名”,但上线前必须规范配置。另外,从2022年开始微信要求涉及用户隐私的接口(比如定位、获取手机号)必须在小程序后台配置《用户隐私保护指引》,并说明收集信息用途,否则接口直接返回错误。
2.3 Android开发环境的版本兼容细节
Android端环境搭建也踩了不少坑。很多人卡在Android Studio版本和AGP版本的对应关系上。比如Android Studio Hedgehog 2023.1.1默认对应的AGP版本是8.2,Gradle版本8.2,要求JDK 17。如果电脑上还是JDK 8或11,同步项目时会直接报错。所以装好Android Studio后第一件事,检查File-Settings-Build Tools-Gradle下的JDK版本,确保是17以上。
| Android Studio版本 | 默认AGP版本 | 默认Gradle版本 | 要求JDK |
|---|---|---|---|
| Hedgehog 2023.1.1 | 8.2 | 8.2 | 17 |
| Giraffe 2022.3.1 | 8.1 | 8.0 | 17 |
| Dolphin 2021.3.1 | 7.3 | 7.4 | 11 |
如果你手头项目是别人给的,打开就报错,八成是Gradle wrapper版本和AGP不匹配。这种问题不要硬调代码,先看两个文件的版本号:项目根目录的build.gradle里classpath 'com.android.tools.build:gradle:8.x.x',以及gradle/wrapper/gradle-wrapper.properties里distributionUrl对应的Gradle版本。两个版本必须满足官方对应关系,这是新手最常见的坑。
Android SDK方面,编译版本用34,目标版本targetSdk也用34,但要注意Android 14(API 34)对蓝牙权限做了调整,后面会专门讲。
3. 核心功能模块与关键实现
3.1 体质辨识问卷:动态题库与判定算法
体质测评模块是整个系统的业务核心,我把它设计成“题目模板 + 判定引擎”的结构,而不是把60道题写死在页面里。数据库里建一张question表,字段包括题目内容、所属体质类型、选项类型、排序号。这样不同版本的问卷(比如完整的60题版、社区筛查用的简化30题版)都可以做成不同模板,后台切换模板前端不用改代码。
问卷页面的交互要注意,九种体质题目数量不一样,平和质8题、偏颇体质7到8题,全部答完大概60题。小程序里我用radio-group实现每个题目的五个选项,得分从“没有(1分)”到“总是(5分)”。提交前必须做完整性校验,漏答任何一题都弹窗提示并定位到未答题目,不然判定结果会出现缺项,转化分算出来就是错的。
判定引擎我单独写了一个Java服务类,核心逻辑是两层循环:外层遍历九种体质,内层累加该体质的条目得分,然后算转化分。这里有几个细节要特别注意,平和质的判定依赖其他八种体质的转化分,所以必须先算完所有偏颇体质再判断平和质;另外,当同时存在多个偏颇体质转化分大于等于40时,会有“复合体质”的倾向,报告里每种体质单独展示,但干预建议要按得分最高的优先排序。代码骨架大概长这样:
public List<ConstitutionScore> evaluate(Map<String, Integer> answers) { List<ConstitutionScore> scores = new ArrayList<>(); for (ConstitutionType type : ConstitutionType.values()) { List<Question> questions = questionMapper.selectByType(type.getCode()); int rawScore = 0; for (Question q : questions) { rawScore += answers.getOrDefault(q.getId().toString(), 0); } double convertScore = (rawScore - questions.size()) * 100.0 / (questions.size() * 4); scores.add(new ConstitutionScore(type.getCode(), rawScore, convertScore)); } return scores; }报告页面展示部分我用canvas画了一张九维雷达图,每个维度对应一种体质的转化分,颜色用渐变填充。雷达图比单纯罗列数字直观太多,居民一眼就能看出自己哪种体质倾向最明显。代码不多,但要注意canvas在真机上需要等canvas-id绑定完成后才能draw,不然偶尔会白屏。
3.2 健康档案与问诊记录:数据库设计
数据库设计决定了系统能走多远,这个项目我建了核心五张表:用户表、体质测评结果表、健康档案表、问诊记录表、随访记录表。用户表存微信openid、unionid、手机号、姓名、性别、出生日期;注意身份证号不能明文存储,最多存脱敏后的版本。
健康档案表记录身高、体重、血型、过敏史、慢病史、用药情况、家族病史,这些是医生开干预方案的参考依据。问诊记录表是业务主表,包含居民ID、医生ID、问诊状态(待接诊/已接诊/已完成)、症状描述、体质调理建议、随访日期。随访记录表关联问诊记录,每次随访写一条,形成完整的随访链路。
这里有一个设计上容易犯的错误:把体质测评结果只当作一个“报告文件”存起来,而不是结构化存储。我的建议是,constitution_result表里一定要有type_code(体质类型编码)、score(转化分)、level(判定等级)这几个字段,这样后续做体质趋势分析时,直接按user_id和时间范围查询就能画折线图,而不是去解析JSON字符串。结构化的数据才是数据资产,一堆文本只能叫记录。
3.3 Android医生端:问诊工作台与蓝牙设备接入
Android端最大的工程在问诊工作台。首页我用CoordinatorLayout + CollapsingToolbarLayout做了可折叠的头部,上面是今日待办统计和轮播Banner,下面是一个TabLayout + ViewPager2,分别承载待接诊、进行中、已完成三个列表。这里很容易遇到嵌套滑动冲突,解决思路是让内部可滚动区域统一使用NestedScrollView或者实现NestedScrollingChild接口的容器,不要让ViewPager2和父级CollapsingToolbarLayout互相抢触摸事件。
医生处理问诊时要能查看居民的历史体质测评结果。我做了折线趋势图,展示居民近几次不同体质转化分的变化,这样医生能直观看到干预方案有没有效果。
蓝牙接入是医生端比较有亮点的功能,可以连接蓝牙血压计和体脂秤,测量数据自动同步到健康档案。BLE开发流程是固定的:扫描设备、连接GATT服务、发现服务、找到特征值、读写数据。核心权限要注意,Android 12以上的targetSdk 31版本就必须在运行时申请BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限,Android 14进一步强化,如果扫描蓝牙设备还需要同时声明neverForLocation标志,否则系统直接拒绝授予扫描权限。
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" tools:targetApi="s" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />实测下来,Android 12以下设备还需要定位权限才能扫描到BLE设备,所以权限申请时要做版本判断,不要一刀切。真机测试时,如果一直扫描不到设备,先检查权限是否全部勾选,再检查蓝牙设备是否处于可被发现状态,最后看手机系统设置里“附近设备”的权限有没有打开。
3.4 小程序端:登录、定位与订阅消息
小程序端登录流程是微信生态里的经典流程,简单说分四步。第一,前端调用wx.login()拿到临时code;第二,把code传给后端;第三,后端拿着code去微信的jscode2session接口换openid和session_key;第四,后端根据openid找到或创建用户,签发自定义登录态token,返回给前端。后续所有接口请求都带上这个token,后端通过JWT做身份校验。
这里有一个共识要纠正:wx.login返回的code有效期只有五分钟,且只能使用一次,不能缓存复用。手机上wifi和流量切换可能导致session_key变化,所以登录态过期后让用户重新走一遍登录流程即可,不要试图自己维护微信侧的态。
手机号授权用button组件open-type="getPhoneNumber",通过e.detail.code换取手机号。注意2023年之后这个接口的返回逻辑有调整,需要用code换手机号,而不是直接拿encryptedData解密,老教程里的写法很多已经失效了。
定位功能这块好几个人问过我,H5页面到底能不能像小程序一样拿到当前经纬度。答案是:如果这个H5是在微信内置浏览器里打开的,可以通过微信JS-SDK的wx.getLocation()获取,前提是公众号已认证、JS接口安全域名已配置、用户授权;如果H5脱离微信环境,只能用浏览器自带的HTML5 Geolocation,而这个接口限制必须HTTPS协议,并且用户拒绝授权就完全拿不到。小程序内部则用wx.getLocation,配置隐私协议后可直接调用。
订阅消息是社区医疗场景里特别有用的功能,用来做体质复测提醒、医生随访提醒。小程序端调用wx.requestSubscribeMessage让用户选择订阅,后端在需要推送的时候调用subscribeMessage.send接口。这里要记住两个硬规则:一次性订阅消息只能推送一次,用户点一次授权只能对应一条消息;长期订阅消息只开放给特定公共服务类目,社区医疗如果资质不足,老老实实做一次性订阅,提醒用户每次操作都点一下授权。
4. 上线前必看的踩坑清单
4.1 微信小程序平台的常见坑
小程序开发中遇到的坑特别集中,我把高频问题整理成了表格,照着排查能省很多时间。
| 现象 | 根本原因 | 解决办法 |
|---|---|---|
| 单选框样式丑、点击区域小 | radio默认样式不可控 | 用radio-group + 自定义view模拟,监听change事件取value |
| 顶部导航栏高度各机型不一致 | 状态栏高度不同 | wx.getWindowInfo().statusBarHeight动态计算,自定义导航栏 |
| iPhone底部操作栏被Home Indicator遮挡 | 未适配安全区 | 全局page添加padding-bottom: env(safe-area-inset-bottom) |
| web-view无法打开公众号文章 | 业务域名未配置或主体类型不支持 | 后台配置业务域名,个人主体只能放弃web-view |
| 真机请求全部失败 | request合法域名未配置 | 后台添加域名,并更新隐私协议 |
| 分包加载不了 | 主包超过2M或者引用路径写错 | 使用分包异步化require.async引入模块 |
| 登录后获取不到用户信息 | wx.login已不支持直接返回用户信息 | 用getUserProfile或open-data组件按需获取 |
单个说一下web-view打开公众号文章这个,需求方很喜欢提,实现限制却很多。web-view组件要求在小程序后台配置业务域名,而且必须是HTTPS,域名根目录要放校验文件。就算配置好了,web-view能打开的页面也必须是该域名下的页面,公众号文章地址在mp.weixin.qq.com这个域名下,同样需要把这个域名加进业务域名列表。个人主体的小程序完全不支持web-view,遇到这种情况只能换方案,比如生成带参数的小程序码跳转到图文页,或者在小程序内部用富文本渲染文章内容。
4.2 Android端适配与权限坑
Android端的问题主要集中在权限适配和文件访问上。社区医生工作台有时候需要从相册选舌象照片上传,Android 7.0之后不能直接用file://路径,必须用FileProvider转换content:// URI。经常有人报错类似content://com.baidu.searchbox.fileprovider这种路径被其他应用接收时抛SecurityException,就是因为没有授权给目标应用。正确做法是通过FileProvider.getUriForFile拿到URI后,再调用grantUriPermission或者Intent里加FLAG_GRANT_READ_URI_PERMISSION。
“android复制”这个看似简单的功能也有讲究,Android 10开始系统会在应用读取剪贴板时弹出隐私提示,如果是自动读取剪贴板很容易被用户拒绝甚至投诉。所以不要在后台静默读剪贴板,只在用户明确点击“复制”按钮时写入剪贴板。
还有些细节,比如Android动态图标主题。Android 13开始支持Themed Icons,图标需要提供monochrome图层,不然在主题化图标开启后会变成一个难看的圆形框。适配就是在mipmap里加一个monochrome层,代码量不大但很显专业度。
4.3 医疗数据的隐私与合规
医疗健康类项目对数据合规的要求比其他类型高很多,这点在开发前就要想清楚。居民的体质测评结果、健康档案、问诊记录都属于敏感个人信息。我的处理方案是三层:传输层全走HTTPS加密,存储层身份证号、手机号做脱敏和加密存储,日志层不打印任何健康数据明文。
小程序端上线前必须在后台配置《用户隐私保护指引》,把收集的信息类型、用途、场景逐项列出来,审核不通过的情况下,像wx.getLocation、getPhoneNumber这些接口会在真机上直接调用失败。Android端同理,所有危险权限弹窗时都要同时展示申请目的,说明文字要尽量具体,比如“用于搜索附近的蓝牙血压计设备”,不要写“用于连接设备”这种模糊描述。
4.4 抓包与调试技巧
联调阶段排查网络问题,抓包是必备技能。小程序开发者工具自带Network面板,可以看到所有请求的URL、入参、返回值,大部分网络问题在这一步就能定位。真机调试时,开发者工具上可以勾选“真机调试”,配合VConsole组件查看Console日志,基本满足日常需求。
如果问题出在Android端,我的习惯是用Charles做代理抓包。手机和电脑连同一个WiFi,手机WiFi设置里填代理地址和端口,然后安装Charles的SSL证书。注意Android 7.0以上默认不信任用户证书,需要用networkSecurityConfig里配置trust-anchors,或者在debug包中开启usesCleartextTraffic,否则只能抓到密文,看不到明文请求内容。这些配置只建议在开发阶段使用,release包务必关闭。
5. 项目总结与后续扩展
5.1 这个系统还能怎么演进
这套架构跑通之后,后续迭代方向其实很清晰。第一个方向是接入更多智能硬件,除了血压计和体脂秤,比如智能手环的心率、睡眠数据也可以采集进来,丰富健康档案的数据维度。第二个方向是报告可视化升级,目前是雷达图加折线图,后续可以加入历史数据对比和同社区同龄人参考范围。第三个方向是给医生端加统计报表,比如按社区维度统计各种体质的分布情况,这些数据对社区健康管理决策很有价值。
后端如果继续演进,可以把体质判定引擎抽成独立服务,做成可配置规则引擎,这样不同机构有不同判定标准时,不用改代码,改配置就能满足需求。整个判定逻辑目前是硬编码在Java类里的,虽然能用,但灵活性偏低。
5.2 给同类项目开发者的一点建议
最后分享几个实践经验,都是踩过坑换来的。第一,信息系统类项目不要纠结入口形式是小程序还是App,先把业务闭环想清楚,居民、医生、管理者三个角色缺一个,系统都是残缺的。第二,体质测评这类核心业务逻辑一定要写单元测试,我当时把九种体质的判定边界值都写成了测试用例,改代码时心里有底,不然上线后被判定结果坑一次,用户信任就没了。
第三,善用AI辅助编程,像Codex这类工具用来生成问卷页面、CRUD接口这类重复性代码效率很高,但涉及医疗业务逻辑的部分必须人工review,别让AI直接决定健康建议的规则。第四,一定要留出测试时间,小程序端和Android端的兼容问题永远比预期多,真机测试从第一天就要开始,不要攒到最后一起测。
现在回看这个项目,真正难的不是代码,而是把中医体质辨识这个偏理论的标准,转化成一套稳定、可验证、能落地的软件系统。希望这篇复盘能帮你少走一些弯路。