☰
微信小程序病历复印申请系统开发实践:前端到支付全流程
2026/10/14 23:14:28 网站建设 项目流程

微信小程序病历复印申请系统开发实践(上)——从前端到全流程实现

最近刚交付了一个面向医院病案室的微信小程序病历复印申请系统,整个过程踩了不少坑,也攒了不少经验。这期先把整体思路、前端页面到全流程打通的过程整理出来,下一篇再单独拆后台管理和打印交付的细节。

先说说这个东西到底解决什么问题。做过医院信息化相关项目的人都知道,病历复印这个场景看起来小,实际痛点非常集中:患者为了报保险、转院、伤残鉴定、司法取证等需求,必须拿到病历复印件,但传统流程要求本人或委托人在工作日跑到病案室窗口排队,填写复印申请单,工作人员再去库里翻纸质病历,按页收费,轻则等半小时,重则来回跑两趟。如果能把“申请、审核、缴费、状态查询”整体搬到微信小程序里,患者手机提交一次,后台按申请单集中处理,体验和效率都能上一个大台阶。这篇内容主要写给两类人:一类是想了解这套业务流程怎么设计的开发者,另一类是医院信息科或第三方软件公司里负责类似需求的产品和技术同学。我会尽量把从零到一的过程,包括选型理由、表结构、接口设计、前端实现和支付流程,讲得直白一点,可以直接照着做。

1. 项目背景与需求拆解

1.1 病历复印的线下痛点

病历复印属于医院病案室的高频业务,但过去二十年基本没什么流程优化。患者到窗口后,工作人员先核对身份,再根据患者姓名和住院号去病案库里找记录,找到后一页一页复印、盖章,按复印页数和份数收费。整个过程有几个硬伤:

第一是时间约束。病案室正常工作时间是工作日朝九晚五,外地患者或者上班族根本没法在工作日专门请假跑一趟。很多患者上午从外地赶来,下午窗口已经关了,只能再多住一晚。

第二是身份核验麻烦。病历涉及个人隐私,医院规定复印时必须核验申请人身份,如果是替别人办,还得提供患者本人的授权委托书和双方身份证。线下办理时经常出现家属带齐了三证却缺一张委托书的情况,白跑一趟。这个环节一旦搬到线上,材料上传的规范化和审核流程成了核心难点。

第三是收费方式落后。按页数计费,价格和实际复印量有关,线下窗口往往是患者先交押金,复印完再按实际页数多退少补,或者现场估算一个大概金额。这种规则放到线上,支付流程就要仔细设计,是先付一次性费用还是先提交申请等后台核算?后面我会专门讲我们的做法。

第四是状态不透明。患者交了申请之后只能被动等电话通知,到底审核过没过、库里有没有这份病历、什么时候能来取,完全靠工作人员口头答复。

这几个痛点摆在一起,线上化的价值就很明确了:申请环节自助化、审核环节后台化、支付环节电子化、结果通知主动化。

1.2 需求清单与角色梳理

需求调研阶段,我们和病案室的老师聊了几轮,把业务角色和需求一条条列了出来。

角色分三类:患者/申请人(小程序端)、病案室审核人员(管理端)、系统管理员(维护端)。小程序端解决申请、上传材料、查询进度、支付和接收通知;管理端解决审核材料、查询患者病历档案、核算复印页数金额、标记打印交付状态;系统管理端负责字典配置、模板参数、用户权限和日志查询。

具体功能需求拆成以下清单:

  • 申请人注册登录:基于微信授权,绑定手机号
  • 新建申请单:填写患者姓名、身份证号、住院号(选填)、与患者关系、复印用途、复印份数、领取方式(自取或邮寄)
  • 材料上传:身份证正反面、授权委托书、其他证明材料拍照上传
  • 后台审核:审核申请信息和材料,确认病历是否存在,估算页数和费用
  • 在线支付:按核算金额生成支付订单,支持微信支付
  • 状态通知:审核通过、支付成功、打印完成、可领取等节点触发订阅消息
  • 申请记录查询:历史申请单列表、详情、状态时间线
  • 管理端数据看板:待审核数量、本月业务量等

需求里有一个容易忽略的点:病历复印件涉及隐私,所以复印出来的内容要不要加水印、是否做脱敏,都是和医院法务、医务科确认后才定的。我们最终的方案是每页复印件叠加半透明水印,水印内容包含申请单号和领取方式,防止复印件被恶意二次传播。

1.3 为什么选微信小程序而不是 H5 或独立 App

这个选择在立项时有过争论,最后定了微信小程序,理由很实际。

首先,用户没有安装成本。患者去医院看病,扫码就能打开小程序,不用下载 App,不用关注公众号,入口极浅。小程序码贴在病案室窗口、导诊台甚至出院结算单上,传播路径很直接。

其次,身份链路相对顺畅。微信生态里的wx.login可以直接拿到 OpenID,配合手机号快速验证,省掉短信验证码这套自建流程。虽然小程序拿不到患者的实名信息,但通过身份证上传和后台人工审核,足够满足病历复印的身份核验要求。

第三,支付能力内置。wx.requestPayment是微信生态原生能力,用户已经习惯在微信里完成支付,体验比跳转外部收银台顺滑得多。

第四,订阅消息能主动触达。审核结果、可领取通知这类信息,小程序可以通过订阅消息推送给用户,这是 H5 做不到的,也是整个流程闭环里很重要的一环。

当然小程序也有劣势:代码包体积限制、审核周期、类目资质要求。尤其医疗类小程序需要提供相关的资质材料,这点在立项阶段就要和医院确认清楚,避免开发完了上不了线。我们项目的解决方案是挂在某医院主体下,用医院的医疗机构执业许可证完成类目认证,这一块一定提前走流程,后台审核花的时间比想象中长。

2. 整体架构与技术选型

2.1 前后端技术栈

前端我们用的是微信原生小程序,没有引入 uni-app 或 Taro。原因很简单:项目核心页面是表单填写、列表查询、图片上传,属于典型的轻交互场景,原生小程序足够胜任。引入跨端框架反而增加了一层编译和兼容性问题,团队里没有人专职做跨端,出了问题排查成本高。

后端选了 Node.js + MySQL。选 Node 而不是 Java 或 PHP,一是团队技术栈匹配,二是这类业务接口没有重计算场景,Node 的异步模型处理文件上传和回调通知很顺手。MySQL 用来存业务数据,表结构清晰,事务支持也够用。文件存储用的是对象存储服务,上传的证件照片和后台生成的水印 PDF 都放对象存储,避免应用服务器磁盘被图片占满。

这里有个细节提醒:小程序要求的请求域名必须是 HTTPS 且在小程序后台配置了合法域名,否则真机调试直接报错。开发阶段可以在小程序后台勾选“不校验合法域名”方便调试,但上线前一定要把域名和证书都准备好。我们当时吃过这个亏,开发环境跑得好好的,换到生产环境一片 request 失败,排查了半天发现是域名没配。

2.2 数据库设计要点

数据库是整个系统的地基,我在设计表的时候优先考虑了三个原则:业务状态可追溯、金额订单可对账、文件存储可关联。

核心表有四张:用户表、申请表、附件表、支付订单表。

用户表结构大致如下:

CREATE TABLE `t_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信openid', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `nickname` varchar(64) DEFAULT NULL, `avatar_url` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

申请表是业务核心表,字段比较多,这里挑关键几个说明:

CREATE TABLE `t_application` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '申请单号', `user_id` bigint NOT NULL, `patient_name` varchar(64) NOT NULL COMMENT '患者姓名', `patient_id_card` varchar(32) NOT NULL COMMENT '患者身份证号', `inpatient_no` varchar(32) DEFAULT NULL COMMENT '住院号', `relation` tinyint NOT NULL COMMENT '申请人关系:1本人 2配偶 3父母 4子女 5其他', `purpose` tinyint NOT NULL COMMENT '复印用途', `copy_count` int NOT NULL DEFAULT 1 COMMENT '复印份数', `delivery_type` tinyint NOT NULL COMMENT '1自取 2邮寄', `address` varchar(255) DEFAULT NULL COMMENT '邮寄地址', `recipient` varchar(64) DEFAULT NULL COMMENT '收件人', `recipient_phone` varchar(20) DEFAULT NULL COMMENT '收件人电话', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1待审核 2审核通过待支付 3已支付待打印 4打印中 5已完成 6已驳回', `total_amount` int DEFAULT 0 COMMENT '金额(分)', `remark` varchar(255) DEFAULT NULL COMMENT '审核备注', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_patient_id_card` (`patient_id_card`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

金额字段我用的是“分”而不是“元”,这个习惯是从支付系统带过来的,避免浮点误差。申请单号建议用“时间戳 + 随机数”生成,不要用自增 id,因为业务上需要走线下沟通,单号越短越好念,我们用的是年月日时分秒加三位随机数,比如20250610143015001。

附件表单独拆出来,因为一个申请单可能有多张图片,每张图片又有业务类型区分:

CREATE TABLE `t_attachment` ( `id` bigint NOT NULL AUTO_INCREMENT, `application_id` bigint NOT NULL, `file_type` tinyint NOT NULL COMMENT '1身份证正 2身份证反 3委托书 4其他', `file_url` varchar(255) NOT NULL, `file_size` int DEFAULT 0, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_application_id` (`application_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

支付订单表单独存支付流水,和申请表通过application_id关联。每个申请单可能只支付一次,所以支付单的表结构里直接记录order_no、transaction_id(微信支付单号)、amount、pay_status、callback_time。对账时主要靠这张表。

2.3 接口设计思路

接口设计遵循一个简单的原则:资源化 + 状态化。每个接口都围绕申请单这个资源展开,前端能做的判断后端都要再做一遍,不能信任前端的任何状态参数。

核心接口清单如下:

接口方法说明
/api/user/loginPOST微信 code 换登录态
/api/application/createPOST创建申请单
/api/application/uploadPOST上传附件
/api/application/listGET我的申请列表
/api/application/detailGET申请单详情
/api/application/payPOST发起支付
/api/application/cancelPOST取消申请
/api/application/notifyPOST订阅消息发送

统一响应格式为{ code: 0, msg: 'success', data: {...} },业务异常用非零 code 标识,前端根据 code 做统一弹窗提示。登录态使用自定义 token,前端存储在wx.setStorageSync,每次请求带在 header 里,后端用中间件校验。

这里有个容易被忽略的点:小程序端的接口幂等性。用户很可能在弱网环境下重复点击提交按钮,申请单会重复创建。我们在前端做了按钮 loading 禁点,后端也做了防重校验,同一用户短时间内相同身份证号的申请直接返回最近一条未完成的申请单。防重不能只靠前端,这是经验。

3. 前端核心页面开发

3.1 登录与身份绑定

小程序端登录流程并不复杂,核心就是wx.login拿 code,后端拿 code 去微信接口换 OpenID 和 session_key,再生成自己的业务 token 返回。

// 登录页核心逻辑 const login = () => { wx.login({ success: async (res) => { if (res.code) { const result = await request({ url: '/api/user/login', method: 'POST', data: { code: res.code } }); if (result.code === 0) { wx.setStorageSync('token', result.data.token); wx.setStorageSync('userInfo', result.data.userInfo); } } } }); };

后端拿到 code 之后调用微信的code2Session接口交换会话信息。这里有几个细节必须说清楚:

  • code是一次性的,用过了就失效,而且有效期只有五分钟。如果前端因为网络问题重试了登录请求,后端要注意捕获重复使用 code 的异常,直接返回友好提示让用户重新wx.login。
  • session_key不需要存在数据库里长期保存,它主要用于解密手机号等敏感数据,解密完成就可以丢弃。我们只在后端内存里短暂持有,不落库,降低泄露风险。
  • 用户是首次登录还是老用户,靠OpenID判断,查不到记录就是新用户,自动注册一条。

手机号绑定那里我用的是button open-type="getPhoneNumber"配合后端解密,不建议用wx.getPhoneNumber直接拿号码,那个接口权限收紧之后个人开发者很难申请下来。医院主体下问题不大,但如果是第三方公司给医院做,资质这块要提前确认。

3.2 预约申请表单设计

表单页是整个小程序里交互最重的页面,字段多、校验多、还关联着后续的审核和计费。我按从上到下的顺序过一遍。

最上面是患者信息区:姓名、身份证号、住院号。身份证号要做格式校验,18 位加最后一位校验位的算法在前端判一次,后端再判一次。住院号是选填,因为很多患者记不住,但填了能大幅减少后台检索病历的时间,我们在 UI 上做了“选填但建议填写”的提示。

接着是申请人关系。关系选项里有一个“其他”选项,选中后要弹出输入框让用户填写具体关系,因为可能出现表姐、堂兄这种近亲属之外的情况。关系字段直接影响后台对委托材料的审核要求,比如“本人”就不需要委托书,其他关系必须上传授权委托书。

用途字段用的是下拉选择,枚举值包括:保险理赔、工伤鉴定、司法诉讼、转院治疗、复诊参考、报销凭证、其他。用途会影响后台审核时对复印范围的判断,比如司法诉讼可能要求整套病历,复诊参考只需要出院小结。枚举值要和后台字典保持一致,我们最初前后端枚举不一致,导致前端显示“司法诉讼”,后台存的是5,后来理了一遍才统一。

份数、领取方式、邮寄地址这些字段相对常规,邮寄地址在领取方式选邮寄时才展示。我们用了小程序的条件渲染,表单联动不算复杂,但要注意地址切换时清空旧值,避免用户从邮寄切回自取再切回邮寄时地址残留。

提交前的整体校验我用了一个通用校验函数,按字段配置规则,规则包括必填、正则、自定义函数三种类型。身份证正则、手机号正则写清楚,不要把校验逻辑散落在各个页面的bindinput里,后期维护会疯掉。

3.3 证件材料上传组件

上传是整个前端最容易出问题的环节,没有之一。身份证照片要清晰可读,委托书要完整拍全,但手机拍照动辄三五兆,直接传原图既慢又容易被小程序上传接口拦下来。

我们封装了自己的上传组件,核心逻辑如下:

const uploadFile = (filePath, applicationId) => { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.uploadFile({ url: `${baseUrl}/api/application/upload`, filePath, name: 'file', formData: { applicationId, fileType: currentFileType }, header: { Authorization: `Bearer ${token}` }, success: (res) => { const data = JSON.parse(res.data); if (data.code === 0) resolve(data.data); else reject(new Error(data.msg)); }, fail: (err) => reject(err) }); }); };

组件内部做了这么几件事:

第一是压缩。拿到临时文件路径后,先用wx.getImageInfo获取图片尺寸,如果长边超过 1280 就调用wx.compressImage压到 1280 以内,质量参数设 80。这样身份证照片一般能控制在 300KB 以内,上传速度在 4G 网络下基本一两秒完成。

第二是并发控制。一个申请单可能同时上传四张图片,一次性全丢上去容易把会话塞满,我们按两张一批上传,中间隔 200ms,既保证速度又不会太激进。

第三是失败重试。上传失败先自动重试一次,重试还失败就提示用户检查网络,保留上传按钮让用户手动重试。不自动重试三次以上,否则弱网环境下会产生一堆僵尸请求。

第四是预览和删除。每张图片上传成功后展示缩略图,支持点击大图预览和删除重传。删除时如果不小心传的fileId和线上对象存储里的文件对不上,后台会留垃圾文件,所以删除接口只做业务数据删除,真正的文件清理用定时任务扫。

上传组件的一个关键细节:设置wx.uploadFile的name字段和后端接收的multer或koa-body的字段名必须完全一致,我们最初 frontend 传的字段名是file,后端写的uploadFile,结果接口一直报参数缺失,排查了半天才发现是大小写问题。这种低级错误最浪费时间。

3.4 申请进度查询与列表管理

申请列表页是用户查看自己申请历史的主要入口,也是整个小程序除提交表单外使用频率最高的页面。列表项需要展示申请单号、患者姓名、状态、创建时间,我的设计是列表页只放核心字段,详情页再展示完整信息和状态时间线。

列表接口采用分页加载,每页 10 条,用onReachBottom触底加载。翻页参数是page和pageSize,返回结果里带hasMore布尔值,前端根据hasMore决定是否继续显示加载提示。这里不要返回total去计算页数,直接返回hasMore更简洁,少一次判断逻辑。

状态展示是列表页的视觉核心。我们把状态和颜色做了映射:待审核用橙色,审核通过待支付用蓝色,已支付待打印用紫色,打印中用青色,已完成用灰色,已驳回用红色。颜色区分比文字更直观,用户瞟一眼就知道自己的申请卡在哪个环节。

详情页要展示状态时间线,我用的是一组静态数据加当前状态高亮。每一条时间线记录包括节点名称、时间、是否在当前状态之前。后端返回statusHistory数组,前端按顺序渲染。这里有个坑:小程序的原生scroll-view和页面默认滚动在某些安卓机型上会有滚动穿透问题,列表页和详情页要设置page的disableScroll或用position: fixed的容器包裹,细节很碎但直接影响使用体验。

4. 全流程实现与关键环节

4.1 状态机设计与流转规则

申请单的状态流转是整个系统的骨架,七个状态之间不是随便跳的,我先把完整流转规则画清楚:

状态含义可执行操作目标状态
1 待审核申请已提交后台审核通过 / 驳回2 / 6
2 待支付审核通过,金额已核算用户支付 / 取消3 / 6
3 已支付支付完成后台标记打印中4
4 打印中正在复印装订后台标记完成5
5 已完成可领取或已邮寄无—
6 已驳回审核未通过用户修改后重新提交1

状态机的实现我们没引入额外的工作流框架,就在申请表上加了一个status字段,后端每次更新时校验当前状态和期望状态的合法性。比如用户支付成功后,回调逻辑里要判断当前状态必须是 2 待支付,才能更新为 3 已支付。如果状态已经是 3,说明发生了重复回调,直接忽略。

这个设计看起来简单,实际运行中非常稳。我们没选择用工作流引擎,因为业务只有一条线性流程,工作流引擎对这种场景属于过度设计,维护成本和性能开销都不划算。

状态变更还有一个隐藏需求:每个状态变更都要记录下来。我们建了一张简单的t_application_log表,字段就是application_id、from_status、to_status、operator、remark、create_time。这张表在用户投诉“为什么被驳回”时非常有用,后台可以明确查到是哪个环节谁做了什么操作。日志表很简单,但价值极高,强烈建议每个业务系统都留一手。

4.2 支付环节接入

支付环节是我们整个项目里技术上最复杂、也最容易出问题的一环。流程是这样的:

用户在小程序端点击“去支付”,前端调后端/api/application/pay接口,后端生成微信支付统一下单请求,拿到prepay_id后拼接支付参数返回给前端,前端再调wx.requestPayment唤起微信支付。

// 前端发起支付 const pay = async (orderNo) => { const result = await request({ url: '/api/application/pay', method: 'POST', data: { orderNo } }); if (result.code !== 0) return; wx.requestPayment({ timeStamp: result.data.timeStamp, nonceStr: result.data.nonceStr, package: `prepay_id=${result.data.prepayId}`, signType: 'RSA', paySign: result.data.paySign, success: () => { // 支付成功,后端回调为准 wx.showToast({ title: '支付成功', icon: 'success' }); refreshDetail(); }, fail: (err) => { // 支付取消或失败 if (err.errMsg && err.errMsg.includes('cancel')) { wx.showToast({ title: '已取消支付', icon: 'none' }); } } }); };

后端生成支付参数的逻辑要特别注意签名串的拼接顺序和字段命名。微信支付最新版本要求nonceStr、package、signType、timeStamp四个字段参与签名,并且签名方式从MD5升级到了RSA或者HMAC-SHA256,我用的是RSA,对应商户 API 证书里的私钥做签名。这块如果搞不清楚,直接报paySign 验签失败,前端拿到的报错还特别隐晦,要把请求参数打出来逐个字段对着文档核对。

支付回调更是重中之重。微信服务器会在用户支付成功后异步通知后端接口,回调地址要在统一下单时通过notify_url参数指定。回调函数拿到参数后必须先做验签,确认请求确实来自微信支付,再根据out_trade_no查对应申请单,核对金额是否一致,最后更新状态。

回调处理有几个必须注意的点:

  • 回调通知可能重复发送,处理逻辑必须具备幂等性。更新状态之前先查一次当前状态,重复通知直接返回成功。
  • 回调接口返回的内容有严格格式要求,必须是微信规定的 XML 格式并返回SUCCESS,否则微信会认为回调失败并持续重试。
  • 回调接口报错不能直接抛出异常,要 catch 住并返回失败标识,让微信继续重试。如果处理逻辑报错了却返回SUCCESS,钱收了单子没更新,就得靠人工对账补救。

我还做了人工对账的辅助页面。后台管理端有一个“支付异常订单”列表,展示已支付但状态未更新的申请单,管理员可以手动核对后点“确认入账”按钮补更新。虽然一年可能用不了几次,但真出事的时候这个功能能救命。

4.3 消息通知与订阅消息

流程闭环少不了消息触达。小程序不像服务号可以随便给用户发模板消息,现在只有订阅消息可用,而且是一次的——用户订阅一次,你只能发送一条消息。

订阅消息的使用策略直接影响触达率。我们选了两个关键节点做消息推送:审核结果通知(通过/驳回),可领取/已邮寄通知。用户提交申请时,前端调用wx.requestSubscribeMessage一次性订阅这两个模板,用户大概率会同意,因为申请刚提交完正处于关注进度的心态。

wx.requestSubscribeMessage({ tmplIds: ['模板ID_审核结果', '模板ID_领取通知'], success: (res) => { // res['模板ID_审核结果'] 的值是 'accept' 表示用户同意 } });

后端在状态变更时调用订阅消息接口,注意两点:

第一,发送订阅消息需要access_token,这个 token 有效期两小时,要缓存并定时刷新,不能每次发送都重新获取。我们是用一个定时任务每 90 分钟刷新一次,存到 Redis,发送时直接取。

第二,用户如果取消了订阅或者订阅已消耗,消息发送会报43101错误,这个错误要被 catch 住并记录日志,不影响主流程。不能因为消息发送失败导致业务状态回滚。

实际运行中订阅消息的打开率还不错,因为内容带申请单号和下一步指引,用户一看就知道干什么。模板内容的字段顺序也会影响阅读体验,建议把最关键的信息(如申请单号、当前状态)放在正文前两行。

4.4 后台审核与打印交付闭环

管理端是整个系统的另一半,直接影响病案室老师愿不愿意用这个系统。我们第一版把管理端做成了小程序内的隐藏页面,通过管理员白名单控制入口,后来觉得审核时表格、图片、操作按钮挤在手机屏上确实不方便,就改成了独立的 Web 管理后台。这里只聊一下审核和交付流程的设计思路。

后台审核界面左边是申请单信息,右边是上传材料预览。审核老师需要确认三件事:材料是否完整清晰、患者身份信息是否一致、申请用途和复印范围是否合理。审核通过后,老师填写实际复印页数和金额,系统自动计算费用并生成支付订单,用户端立刻进入待支付状态。

这里有个业务细节:复印费用 = 实际页数 × 每页单价 × 份数,另加邮寄费(如果用户选择邮寄)。单价字段做成系统参数,不同医院价格不同,医院调价时不改代码。邮寄费也做成按地区的阶梯价,后台维护一个简单的运费表。

打印交付环节的流程是:用户支付完成后,申请单进入待打印队列,老师从病案系统里导出对应患者的病历 PDF,在后台点“开始打印”,系统在 PDF 每页叠加水印后提交到打印任务,打印装订完成后点“完成”,如果用户选了邮寄再填快递单号。这里水印的生成我们用了服务端的 PDF 处理库,性能上完全够用,几十页的 PDF 水印叠加在秒级完成。

打印交付完以后,还有最后一个环节:领取时核验身份。自取用户在窗口出示小程序里的取件码,老师核对取件码和申请单一致后交付。取件码在申请完成后生成,和申请单号绑定,相当于线下提货凭证。这个细节让病案室老师非常满意,因为以前要人工翻记录核实,现在扫一眼取件码就行。

5. 常见问题与排查实录

5.1 图片上传的连环坑

图片上传这块我们前后改了三个版本,中间踩的坑值得单独拿出来说。

第一个坑是wx.uploadFile的filePath必须是临时文件路径。从相册选择图片得到的tempFilePath可以直接用,但如果先做了压缩,就必须用压缩返回的新路径,不能继续用旧路径。很多人忽略这一步,上传后图片是坏的,后台预览打不开。

第二个坑是上传请求的content-type。wx.uploadFile是 multipart 表单上传,后端如果用bodyParser解析会直接报错,必须用专门的 multipart 处理中间件。调试时如果看到后端收到空对象,八成是这里的问题。

第三个坑是小程序上传有大小限制,虽然wx.uploadFile的默认限制实际是 10MB,但身份证照片经过压缩后一般在 200KB 左右,问题不大。真正要注意的是后台服务器和对象存储的单文件大小限制,我们配置的是 5MB,超过直接拒绝。前端在上传前判断文件大小,超过限制提示用户重新拍照,比等服务器返回错误再提示体验好得多。

第四个坑是安卓手机拍照的图片方向问题。部分安卓机型拍出来的照片带 EXIF 旋转信息,上传到后台后显示是倒着的。这个问题我查了很久,最后是在前端读取图片时调用wx.getImageInfo获取宽高判断是否大于原图的宽高,然后在前端做一次旋转处理,或者让后端在上传后统一用图像处理库把 EXIF 信息抹掉并转成标准方向。两种方案都能解决,我们前后端各做了一道,确保万无一失。

5.2 登录态过期与静默登录

登录态是所有请求的前置条件,这里出的问题影响面最大。

小程序的wx.login不是每次调用都能拿到有效 code,有时候因为网络问题会失败。如果登录失败就白屏或者一直转圈,用户体验很差。我的做法是写了一个ensureLogin方法,检查本地 token 是否存在且未过期,不存在才调用wx.login重新获取。

token 过期的问题更隐蔽。我们设置的 token 有效期是 30 天,但用户可能一个月后才再次打开小程序,此时 token 已经过期。请求失败返回 401,前端统一拦截后调用静默登录刷新 token,刷新成功后再重新发起原请求。这个逻辑要在封装的 request 方法里统一做,不能在每个页面单独写。

还有个坑:微信会清理小程序的本地缓存,wx.setStorageSync存的东西可能莫名其妙消失。如果 app.js 的启动逻辑依赖本地 token,一定要加判断,token 不存在就走首次登录流程,而不是直接跳转到需要登录的页面。

5.3 支付回调丢失与对账兜底

支付回调丢失是我们测试时真实遇到过的场景。用户明明付了款,但微信的异步通知因为网络抖动没到达我们的服务器,申请单一直停留在待支付状态,用户打电话来投诉。

排查这个问题的思路是:不能靠用户主动反馈,要建立主动对账机制。我们写了一个定时任务,每 30 分钟拉取一段时间内状态为待支付且创建时间超过 1 小时的申请单,去调用微信支付查询订单接口确认支付状态。如果微信侧显示已支付,就主动补更新申请单状态,并给用户发送订阅消息告知支付成功。

定时任务跑了一段时间后,我们发现它不仅能解决回调丢失的问题,还能发现另一类问题:支付金额和订单金额不一致。虽然微信支付的回调和查询接口都有金额字段,但人写的代码总会有漏洞,定期扫一遍异常金额订单作为兜底,比用户事后发现再补处理省心得多。

另外提醒一句,支付回调的日志一定要保留原始报文,包括 headers 和 body,排查问题时这些是最直接的证据。我们当时为了排查一个签名问题,翻了三天的回调日志才找到规律——某个字段在回调中偶尔会多一个空格,导致签名校验失败。这种问题不保留原始报文是根本发现不了的。

6. 上线前后的几点经验心得

项目从启动到上线大约花了两个月,团队三个人,一个负责小程序前端,一个负责后端接口,一个负责和医院对接需求。整体节奏不算快,但中途确实踩了一些不应踩的坑,写在这里给大家做个参考。

第一,需求确认阶段一定要把“复印用途”和“复印件范围”的逻辑理清楚。我们第一版把用途和范围做成两个独立字段,后来发现这对后台审核人员是额外负担,他们要根据用途去判断该印哪些页码。最终改成了用途驱动范围的方式,审核界面直接显示“本用途默认复印范围为:……”,节省了不少培训成本。

第二,医院环境的网络和服务器条件比想象中差。很多医院的互联网出口带宽有限,如果病历 PDF 存在本地服务器,后台打印时频繁读取大文件会拖垮网络。我们的方案是把病历 PDF 提前同步到对象存储,后台打印直接从对象存储读取,应用服务器和医院内网之间的流量压力小了很多。这一点在部署时就要考虑清楚,不要等上线后被打爆了再改架构。

第三,小程序端的用户体验细节决定了好评率。比如表单填写过程中切后台再回来,页面数据不能丢;比如上传图片时显示进度;比如弱网环境下所有接口要有 loading 状态和超时提示。这些小事单个看不起眼,但对年纪偏大的患者群体来说,多一个明确提示就能少一次困惑。我们上线后收到的最多反馈不是功能缺失,而是“页面转圈不知道现在到底成没成”。

第四,管理端一定要支持批量操作和检索。病案室高峰期一天可能处理几十单待审核申请,如果后台只能一个一个点开查看,老师会用脚投票回到线下流程。我们给管理端加了列表模式,一屏显示多个申请单的摘要信息和状态,支持按身份证号、申请单号、状态筛选,审核动作做了快捷键。这个小改动直接决定了系统能不能被业务方接受。

第五,灰度上线比一次性全量稳妥。我们先在某医院的一个病区试点了两周,收集真实患者的反馈后再全医院推开。试点期间发现的问题包括:一部分老年患者不会上传身份证正反面、个别机型上传身份证照片后系统显示模糊无法审核、部分使用场景下用户误选了“其他”关系导致需要补交委托书。这些问题全部在试点期修复后才全面开放。

最后说一句实在话。病历复印申请这种业务,技术本身并不难,难点都在业务流程的理解和细节的把控上。状态怎么流转才不会乱、费用怎么核算用户才不投诉、材料怎么审核才能既合规又高效,这些才是这个项目的核心价值。这篇先把小程序端到支付闭环的内容整理完了,第二篇我会重点写管理端的表格设计、打印水印的实现细节,以及我们怎么用定时任务解决对账和超时清理的问题,有需要的朋友可以继续关注。

如果你也在做类似的医院服务线上化项目,建议尽早拉通需求方一起走一遍完整的申请流程,角色扮演一次患者和一次审核老师,你会发现很多坐在办公室里根本想不到的问题。流程走通了,代码只是水到渠成的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询