
去年我们团队把跑了大半年的H5版远程在线诊疗系统整体重构成了微信小程序整个过程从技术选型到上线维护踩了不少坑。这套系统核心解决的问题很朴素患者不用到现场排队就能挂号、问诊、看报告医生在排班时间可以在线接诊并开电子处方费用通过小程序内支付实时结算。整个项目包含患者端小程序、医生端工作台和运营管理后台三块覆盖预约挂号、图文问诊、视频问诊、电子病历、处方流转、在线支付、药品配送对接等十几条业务链路。这篇文章不打算讲空泛的架构理论而是把新手最容易卡住的细节——请求封装、缓存时效、导航栏适配、支付接入、审核合规、线上问题排查——单独拎出来给出可以直接抄作业的写法。如果你正在做医疗类小程序或者准备把传统H5问诊业务小程序化这篇会比较对口。1. 项目概述与需求拆解1.1 核心需求拆解先把线下就诊流程翻译成在线流程医疗行业有一个特殊性稳定和安全优先于一切功能再花哨也不能牺牲流程的严谨性。远程在线诊疗系统的本质是把线下的“挂号—候诊—问诊—开方—取药—支付”这一整条链路搬到线上同时在每个环节增加可追溯的状态记录。从角色维度拆分需求非常清晰角色核心诉求关键功能患者足不出户完成看病预约挂号、候诊排队、图文/视频问诊、报告查询、电子处方、在线支付医生高效接诊、规范开方排班管理、接诊队列、病历书写、开具处方、与患者沟通管理员运营监控与数据统计医生管理、科室管理、订单统计、药品目录维护、退款处理功能模块定了之后紧接着要梳理业务边界哪些功能必须在线完成哪些需要线下配合。比如抽血化验、影像检查这类物理环节在线系统只能做到“预约到院”不能替代现场执行。我们在需求评审时把这类流程单独标记为“线上预约、线下履约”避免开发时把系统做成一个不切实际的“全在线医院”。1.2 业务流程设计状态机是问诊系统的命根子在线问诊最怕的是业务状态混乱患者付了费医生没接诊医生开了方支付回调没到账患者取消订单候诊队列没同步……这些问题全是状态机没设计好导致的。我们的核心流程是这样的患者提交挂号/问诊申请系统创建订单状态为“待支付”支付成功后状态变为“已预约”同时进入医生的接诊队列医生点击接诊状态变为“问诊中”此时患者端显示医生在线状态医生结束问诊并填写病历状态变为“待开方”或“已完成”若开具电子处方患者确认后进入“待支付药品费”药品费支付完成订单进入“配送中/待取药”最后闭环在“已完成”。每一步状态流转都对应后端接口的显式请求前端不能用本地变量记录订单状态必须实时从接口拉取。这里有一个很关键的教训问诊中的消息推送我们一开始只依赖WebSocket结果弱网环境经常断线导致患者收不到医生回复。后来改成“WebSocket实时推送 接口轮询兜底”双通道方案虽然多写了一点代码但消息可靠性提升非常明显。医疗场景里“消息没送到”比“消息慢几秒”严重得多宁可冗余不能漏。2. 技术选型与工程结构设计2.1 原生微信小程序还是 uniapp没有标准答案只有合适答案这个是项目启动时争论最多的问题。我们的团队规模是两个前端同时要维护微信小程序和后续可能要上的App端所以技术选型直接关系到人力能不能撑得住。对比维度原生微信小程序uniapp Vue多端复用只能跑微信代码无法直接迁移一套代码可以打微信小程序、H5、App、鸿蒙等性能表现渲染效率最优复杂列表更顺滑中间层有转换损耗极致性能需要优化生态与组件微信官方能力调用最直接uni-app插件市场丰富但要甄别质量学习曲线需熟悉WXML/WXSS/小程序API会Vue就能上手上手更快原生能力颗粒可以直接用wx.xxx全部能力部分能力要查条件编译、看平台差异我们最后选了uniapp。原因很实在问诊系统有大量表单页面、列表页面和流程页面用Vue的组件化开发效率比原生WXML高很多而且同一个患者端以后还想覆盖支付宝小程序和独立的App不可能每个端都重新写一遍。代价是遇到平台差异问题时需要花时间做条件编译和处理兼容这部分我在后面“常见问题”里会详细讲。但如果你只做微信端、且团队对原生小程序已经非常熟练那我建议直接用原生。uniapp的价值在跨端不在单端性能。单端场景用原生更省心。2.2 工程目录与分包设计主包只放核心路径项目采用uniapp标准目录但根据业务做了分层src/ pages/ # 主包页面首页、登录、就诊人管理、订单列表 pages-doctor/ # 分包1医生工作台、接诊、病历、开方 pages-live/ # 分包2视频问诊、聊天会话 components/ # 公共组件医生卡片、订单卡片、空状态 api/ # 接口请求模块按业务域拆分 utils/ # 缓存、导航栏适配、格式化等工具函数 store/ # 全局状态管理 static/ # 静态资源这里强烈建议做分包。我们首版把所有页面都塞进主包开发时没感觉一上线发现冷启动慢得明显尤其是低端安卓机体验很糟糕。后来问诊室、视频通话、支付结果这些重页面全部拆到分包主包只保留首页、登录、订单等核心路径启动耗时降了差不多三分之一。小程序有主包体积限制医疗类系统又经常要上传报告图片、聊天素材分包是必须做的事不是可选项。3. 关键模块的实现细节3.1 请求封装与统一拦截别让每个页面自己调wx.request一个真实项目少说几十个接口如果每个页面都自己写一遍wx.request光错误处理就能写出三套不同版本。我们的做法是封装一个统一请求方法全局只认一个出入口所有接口模块都基于它构建。// api/request.js const BASE_URL https://api.yourdomain.com; function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || }, success: (res) { const { code, data, message } res.data || {}; if (code 0) { resolve(data); } else if (code 401) { // 登录态失效统一跳登录并清理本地凭证 uni.removeStorageSync(token); uni.removeStorageSync(userInfo); uni.navigateTo({ url: /pages/login/index }); reject(res); } else { uni.showToast({ title: message || 服务异常, icon: none }); reject(res); } }, fail: (err) { uni.showToast({ title: 网络连接失败请稍后重试, icon: none }); reject(err); } }); }); } export default request;封装的好处不只在于少写代码。统一拦截器让我们可以集中处理登录态失效、错误提示、埋点上报甚至后续要做请求队列、接口加密都只需要改这一个文件。这里有个细节接口返回的code与HTTP状态码要分开处理。我们后端约定HTTP层只返回200业务状态码放在body里这样前端拦截器和后端网关可以解耦排查问题也会清楚很多。3.2 缓存策略与登录态时效小程序storage没有自动过期机制微信小程序的storage和浏览器localStorage一样本身没有过期时间概念。如果不做处理token、用户信息这些数据就会一直累积在那里轻则数据陈旧重则登录态过期后用户还在调用受保护接口。我们封装了一个带有效期的小工具所有本地缓存统一走这个入口// utils/cache.js export function setCache(key, value, expireSeconds 0) { uni.setStorageSync(key, { value, expire: expireSeconds ? Date.now() expireSeconds * 1000 : 0 }); } export function getCache(key) { const data uni.getStorageSync(key); if (!data) return null; if (data.expire data.expire Date.now()) { uni.removeStorageSync(key); return null; } return data.value; } export function removeCache(key) { uni.removeStorageSync(key); }实际业务里不同数据的有效期策略差别很大登录token2小时有效期配合后端每次请求校验刷新不能让token永久有效用户基础信息昵称、头像、手机号24小时医生排班列表5分钟避免每次进入都刷全量数据药品分类目录一周这类低频变化数据可以缓存久一点处方、病历、报告不做本地持久化只存在内存中页面销毁就释放。敏感医疗数据留在本地有泄漏风险这是合规红线。3.3 顶部导航栏与自定义顶部的适配全面屏时代的经典坑微信小程序里做自定义顶部导航最大的坑是不同机型的刘海屏和状态栏高度不一致。iPhone X以上有刘海Android各家全面屏手势区的处理也五花八门如果写死导航栏高度马上就会出现标题被状态栏压住或者自定义按钮离胶囊按钮太近的情况。适配方案的核心是拿到胶囊按钮的位置再反推导航栏高度。小程序提供了getMenuButtonBoundingClientRect拿到胶囊按钮的top和高度配合状态栏高度就能算出导航栏总高度// utils/navbar.js export function getNavBarInfo() { const winInfo uni.getWindowInfo(); const menuRect uni.getMenuButtonBoundingClientRect(); const statusBarHeight winInfo.statusBarHeight || 20; let navBarHeight 44; if (menuRect) { // 胶囊上下边距相等导航栏高度 胶囊top与状态栏的间距 * 2 胶囊高度 navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.height; } return { statusBarHeight, navBarHeight, menuRect }; }这个公式的原理很简单微信小程序的胶囊按钮在导航栏里垂直居中所以胶囊按钮顶部到状态栏底部的距离乘以2再加上胶囊自身高度正好等于导航栏的总高度。我们把这个工具函数放在全局每个需要自定义顶部的页面都从这里取高度再结合CSS变量动态设置padding实测从iPhone SE到最新的Pro机型、各种安卓机型都没有再出现错位问题。3.4 表单组件与交互细节单选框的合理替代方案诊疗系统里大量用到表单性别、科室、支付方式、预约时段都需要单选。小程序的radio组件功能没毛病但原生的样式很难看而且跨端在安卓和iOS上的渲染差异明显。这里分享一个我们在实际项目里验证过的做法选项不超过3个的场合直接用自定义按钮组模拟单选。比如支付方式选择我们用三个卡片按钮选中时切换边框色和背景色底部加选中状态的打勾图标交互反馈比原生radio清晰得多。核心代码如下view classpay-option :class{ active: payMethod wx } clickpayMethod wx text classpay-name微信支付/text text classpay-check v-ifpayMethod wx✓/text /view靠CSS控制选中态配合一点过渡动画视觉一致性更好也避开了radio组件在部分Android机型上显示变形的问题。如果选项很多、需要用到picker滚动选择建议用小程序内置picker组件比手写滚动选择器稳定。4. 核心业务流程实操记录4.1 在线问诊主流程支付到接诊的链路要环环相扣在线问诊是整套系统的主动脉我们花时间最多的地方是“患者支付成功到医生开始接诊”这中间的状态同步。我们的做法是支付成功后前端立刻调用后端“确认支付结果”接口后端确认订单状态并写入医生队列医生端通过长连接收到新接诊提醒同时工作台会轮询拉取最新队列数据。主流程实现分四步患者选择医生和时段提交预约单创建订单支付成功后订单状态流转为“已预约”医生端出现待接诊卡片医生点击“开始接诊”患者端弹出聊天/视频窗口双方进入问诊会话医生填写病历并提交处方可选患者端收到处方单确认后进入药品支付。这里我要特别提醒一个容易忽略的点视频问诊一定要做“断线重连”和“排队兜底”。我们线上跑的时候遇到过一次视频服务商短暂故障患者端黑屏医生端的会话状态还停在“问诊中”两边都不知道发生了什么。后来我们加了心跳检测30秒无响应就在界面上提示网络异常并支持一键切换到图文问诊继续沟通。这个备用方案看着不起眼但真到生产环境就是救命的。4.2 微信支付接入个人主体绕不开的资质门槛在线诊疗必然涉及收费微信支付是绕不开的环节。首先要明确个人主体的小程序无法开通微信支付必须使用企业主体或个体工商户主体申请微信支付商户号。医疗信息服务类类目对资质审核更严格这个问题要在项目立项时就想清楚不然开发到一半才发现无法支付返工成本极高。支付流程设计上遵循微信官方的“服务端统一下单 前端调起支付”模式患者点击支付前端向后端发起“创建支付订单”请求后端调用微信支付统一下单API拿到prepay_id后端使用商户私钥生成支付签名把timeStamp、nonceStr、package、signType、paySign返回给前端前端调用uni.requestPayment传入上述参数支付结果由微信支付官方回调通知后端后端验签并更新订单状态。前端核心代码很简洁但后端签名和回调验签必须严格按文档来// 前端调起微信支付 uni.requestPayment({ provider: wxpay, timeStamp: payData.timeStamp, nonceStr: payData.nonceStr, package: payData.package, signType: RSA, paySign: payData.paySign, success: () { // 支付成功等待后端回调确认后刷新订单 this.refreshOrderStatus(); }, fail: (err) { // 用户取消或支付失败别急着删订单让用户选择重新支付或取消 } });这里要强调前端只负责调起支付绝对不能自己决定订单是否支付成功。我们曾经发现部分异常场景下前端success回调触发了但后端回调没收到导致订单卡在“已支付”和“未支付”的中间态。后来统一改成前端收到success后立即调后端“查询订单状态”接口以数据库里的最终状态为准。前端弹窗提示和页面跳转都基于这个接口的结果这样任何一环出现问题都能正确回到订单页。4.3 实名认证与身份证信息提取合规收集必须过授权关在线问诊涉及人身安全问题患者实名认证是硬要求。小程序里实现“扫描身份证提取身份证号”一般有两条路一条是前端拍照/上传身份证图片传给后端OCR识别服务另一条是直接接入第三方实名认证服务商的SDK由服务商完成识别和活体检测。我们采用的是后者方案更稳妥合规压力也小一点。不管用哪种方式有两个底线必须守住第一收集身份证信息前必须通过弹窗获得用户明确授权说明用途和使用范围。不能在用户无感知的情况下静默识别证件这违反了个人信息保护的基本要求。第二证件原图不能长期存储在后端服务器。OCR识别完成后我们会立刻裁剪掉敏感区域原图在完成识别后定时清理。身份证号、姓名这类敏感字段在传输过程中要做加密日志中做脱敏处理只显示后四位。前端实现上小程序里直接用chooseMedia调起相机拍摄拍摄完成后再上传到识别接口整体交互就是“拍照—上传—回填表单”三步// 拍摄身份证并提取信息 uni.chooseMedia({ count: 1, mediaType: [image], sourceType: [camera], success: (res) { const tempFilePath res.tempFiles[0].tempFilePath; // 上传临时文件到后端OCR接口 uploadForOcr(tempFilePath).then((info) { // 回填姓名、身份证号等字段 form.name info.name; form.idCard info.idCard; }); } });4.4 发布审核与合规医疗类小程序的审核比普通应用严格得多远程在线诊疗属于微信小程序里医疗类目下的敏感类目不是随便一个企业主体就能开通。按平台规则涉及在线问诊、电子处方的需要提供《医疗机构执业许可证》或相关资质证明具体的类目选择直接决定审核能不能过。我们在这上面耽误过两周原因是资质文件主体和小程序主体不一致审核被驳回。所以提到资质这件事一定要在域名配置和类目选择之前就确认清楚主体一致性是硬条件。另外提审前的自查清单里隐私保护指引是重头戏。小程序后台需要配置隐私协议弹窗明确列出收集用户信息的目的和用途。我们第一次提审就是因为隐私指引里漏了“身份证号”这一项被驳回了。微信官方对敏感信息的收集非常敏感医疗应用尤其如此。还有一个平台规则容易忽略小程序年审。我的小程序主体资质到期或营业执照更换都需要在后台完成年度审核不然线上版本可能被限制使用。很多独立开发者产品上线后就不管后台了结果年底突然被下架恢复流程又很麻烦。5. 常见问题与排查技巧实录5.1 iOS机型网络请求失败率高错误码6001的坑上线一段时间后我们监控发现iOS设备上的接口请求失败率明显高于Android尤其集中在老版本iOS系统错误码频繁出现6001。排查路径大概有四步确认网络环境6001多数是TLS握手失败或证书校验不过先排除手机本身网络问题检查HTTPS证书链是否完整部分iOS机型对证书链完整性要求比Android更严格证书中间链缺失会导致握手失败检查后台域名白名单小程序request合法域名必须在后台配置而且必须HTTPS检查ATS合规性iOS对非HTTPS请求有限制但小程序层面已经强制HTTPS主要排查还是证书配置。我们最后的根因是服务器证书使用了旧版TLS配置部分老版本iOS默认不兼容。升级证书配置后iOS端失败率从2%降到了0.2%左右。这个问题给我们的经验是医疗应用的用户群体年龄偏大老旧机型的使用比例比你想象的高开发阶段就要兼容够老的系统版本不能拿自己的新手机测一遍就上线。5.2 开发调试技巧本地抓包和热刷新的正确打开方式微信小程序开发工具自带的Network面板已经能看大部分请求数据但真机测试时想看线上环境的具体接口返回抓包工具还是很有用。我常用的场景是用户反馈某个页面数据加载不出来本地又无法稳定复现就通过抓包工具拦截真机的小程序请求对比返回数据和正常接口的差异快速定位是前端传参问题、后端返回异常还是网络层故障。操作上用代理工具将PC和手机置于同一局域网手机设置代理指向PC小程序开发版请求就会经过代理工具在代理界面里能看到完整请求头、请求参数和响应内容。注意抓包只用于开发调试不能用它做任何非合规的事情更不能拿线上敏感数据做其他用途。开发效率方面微信开发者工具的热刷新很好用。但是uniapp项目要注意修改了utils目录下的公共文件热刷新可能不生效需要手动重新编译。我一般改完公共模块直接CtrlB手动编译避免页面状态残留导致问题。还有一个小技巧开发版小程序里打开“不校验合法域名”选项可以临时调试未配置白名单的本地接口但上线前一定要关掉否则审核会被拒。5.3 跨端兼容差异uniapp不是“一次编写到处运行”的银弹uniapp宣传的口号很美好但实际开发中平台差异依然存在。我们踩过的几个具体问题微信小程序端的storage和App端的storage完全是两套实现App端uni.setStorageSync同步写入本地文件小程序端走的是微信的storage机制数据量上限和行为都不一样。所以缓存工具函数最好自己封装不要在业务层直接调用uni的storage方法。视频问诊组件在Android和iOS上的权限表现不一致。Android需要动态申请摄像头和麦克风权限iOS则通过系统弹窗询问。我们用条件编译分别处理了权限申请逻辑// #ifdef APP-PLUS // App端使用plus.android.requestPermissions申请权限 // #endif // #ifdef MP-WEIXIN // 小程序端使用wx.authorize申请权限 // #endif这是条件编译的真实应用场景。如果项目只在微信小程序里跑不需要考虑这些但如果以后要做App和鸿蒙一开始就预留条件编译的处理能省掉后面大量重构时间。5.4 性能优化与并发控制小程序的10个并发请求限制微信小程序的并发请求上限是10个超出部分的请求会排队如果排队时间太长用户侧直观感受就是“卡”。在线诊疗系统的特点是页面打开瞬间会同时发起多个请求用户信息、挂单列表、医生排班、消息未读数很容易在首屏阶段触到并发上限。我们的优化策略分三层首屏只加载核心数据。首页优先拉用户基本信息推荐医生排班列表和消息未读数等二次加载公共数据走缓存。科室分类、医院列表这类不常变的数据用前面说的带有效期缓存直接省掉一个请求图片懒加载。医生头像、药品图、报告缩略图统一走懒加载减少网络请求阻塞。这里还要提一个容易被忽视的点小程序页面切换时前一个页面的异步请求如果还没返回有可能触发setData报错。我们在请求封装的外层做了页面生命周期关联校验页面onUnload时中断该页面未完成的请求的状态更新避免“页面已销毁还在弹Toast”这种低级但是真实的线上问题。做完整套系统回头看最大的体会是远程在线诊疗这种业务技术本身反而不是最难的最难的是把复杂的线下流程转译成线上状态机同时把稳定性和合规性焊进每一个细节。开发阶段多花时间把缓存、异常、过期这些边角处理干净上线的日子就会好过很多。后面我们还计划把用药提醒和复诊随访模块做成订阅消息和公众号联动这套基础架构应该还能继续撑一段时间。