资讯详情 微信小程序体检系统全栈开发:预约排队报告档案与健康数据分析
📅 2026/10/22 15:08:15
简介这是一套面向医疗信息化开发者与计算机专业学生的微信小程序项目源码聚焦医院体检场景的全流程数字化管理适合作为毕业设计、课程实训或二次开发的学习素材。系统覆盖在线预约体检套餐、实时排队进度查看、电子报告查询、健康档案管理、医生在线咨询、体检注意事项提醒及健康数据分析追踪等模块前后端结构完整。压缩包共1201个文件约15.31MB包含163个js、128个vue、111个java等后端与前端逻辑代码80个wxss与78个wxml构成小程序界面另有232个png、162个svg、64个jpg等图片资源及88个json配置、1个sql脚本便于理解项目分层与数据表设计。目前已有189人学习下载。源码可直接运行调试帮助读者掌握微信小程序与Java服务端的联调方式梳理体检业务流程的数字化实现思路并在此基础上进行功能扩展与个性化改造。1. 体检流程数字化的真实痛点为什么小程序是当前最务实的入口体检这件事用户端体验和医院端管理长期是割裂的。用户拿着纸质指引单在科室间来回问路医生在系统里反复切换界面确认项目状态报告出来之后还要专门跑一趟医院去取。做过体检系统的人都知道真正难的不是某一个功能而是把预约、排队、报告、档案、咨询这几件事串成一条不中断的链路。微信小程序之所以成为当前最务实的入口原因很直接用户不需要装 App扫码即用微信支付和消息通知天然打通医院侧只需要维护一套后端服务。这个标题指向的正是这样一套系统——在线预约体检套餐、实时查看排队进度、电子报告查询、健康档案管理、医生在线咨询、体检注意事项提醒、健康数据分析追踪全部围绕体检流程数字化展开。适合谁做中小型体检机构的信息化负责人、承接医院外包项目的开发团队、以及想用一套完整案例练手全栈开发的人。下面我按实际落地顺序把选型、数据模型、核心接口和踩坑点拆开讲。2. 技术选型与数据模型先把骨架搭对再写页面2.1 为什么是微信小程序加自建后端而不是纯云开发常见做法有两种一是用微信云开发数据库、云函数、存储全托管二是小程序前端加自建后端服务。我一般会推荐后者原因有三个。第一体检数据涉及个人健康信息很多机构要求数据落在自己可控的服务器上云开发的数据归属和迁移成本在合规审查时是个麻烦。第二排队进度需要 WebSocket 或轮询推送云函数在长连接场景下限制较多。第三体检套餐的库存、排班、报告生成这些逻辑往往要和医院已有的 HIS 或 LIS 系统对接自建后端更容易做适配层。技术栈上前端用微信小程序原生框架或 uni-app 都可以后端我倾向 Node.jsExpress 或 Koa或 Java Spring Boot数据库用 MySQL 加 Redis。Redis 用来做排队队列和热点缓存MySQL 存业务数据。下面是一个最小可用的项目结构先把这个骨架建起来后面所有功能都往里填。# 项目根目录结构后端部分 health-exam-server/ ├── src/ │ ├── config/ # 数据库、Redis、微信 appid/secret 配置 │ ├── controllers/ # 预约、排队、报告、档案、咨询各模块控制器 │ ├── models/ # Sequelize 或 TypeORM 数据模型 │ ├── routes/ # 路由定义 │ ├── services/ # 业务逻辑层排队算法、报告生成在这里 │ ├── middlewares/ # 鉴权、日志、错误处理 │ └── utils/ # 微信登录解密、日期处理等工具 ├── package.json └── .env # 敏感配置不入库这个结构的关键在于 services 层要独立出来。排队进度的计算、报告 PDF 的生成、健康数据的聚合分析这些逻辑不应该写在 controller 里否则后面接 HIS 系统时会改得很难受。2.2 核心数据表设计五张表撑起全流程体检系统的数据模型不复杂但字段设计要考虑扩展。下面给出五张核心表的建表语句覆盖套餐、预约、排队、报告、档案五个域。-- 体检套餐表 CREATE TABLE exam_package ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 套餐名称, items JSON NOT NULL COMMENT 检查项列表JSON数组, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0 COMMENT 每日可预约人数, notice TEXT COMMENT 注意事项, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 预约订单表 CREATE TABLE appointment ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, package_id INT NOT NULL, exam_date DATE NOT NULL COMMENT 体检日期, time_slot VARCHAR(20) COMMENT 时间段如08:00-09:00, status TINYINT DEFAULT 0 COMMENT 0待体检 1进行中 2已完成 3已取消, queue_no INT COMMENT 排队号, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_date (exam_date) ); -- 排队进度表Redis 为主MySQL 做持久化备份 CREATE TABLE queue_record ( id INT PRIMARY KEY AUTO_INCREMENT, appointment_id INT NOT NULL, dept_code VARCHAR(20) NOT NULL COMMENT 科室编码, queue_no INT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0等待 1叫号中 2已完成, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_appointment (appointment_id) ); -- 电子报告表 CREATE TABLE exam_report ( id INT PRIMARY KEY AUTO_INCREMENT, appointment_id INT NOT NULL, report_url VARCHAR(255) COMMENT PDF存储路径, summary JSON COMMENT 各检查项结果摘要, doctor_advice TEXT COMMENT 医生建议, is_abnormal TINYINT DEFAULT 0 COMMENT 是否有异常项, published_at DATETIME, INDEX idx_appointment (appointment_id) ); -- 健康档案表 CREATE TABLE health_archive ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, report_id INT NOT NULL, indicators JSON COMMENT 关键指标快照用于趋势分析, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id) );这里有几个设计决策值得说明。套餐的检查项用 JSON 存而不是单独建关联表是因为体检套餐的检查项数量有限且不常变JSON 查询在 MySQL 5.7 以上够用省掉一次 join。排队记录同时写 Redis 和 MySQLRedis 负责实时读取MySQL 负责断电恢复和对账。健康档案里的 indicators 字段存的是从报告里抽取的关键指标快照比如血压、血糖、血脂这些后面做趋势分析时直接查这张表不用每次去解析 PDF。2.3 微信登录与用户体系的最小实现小程序端的用户身份靠 wx.login 拿到的 code 换 openid后端用 openid 做唯一标识。这里不展开微信官方流程只给后端换取 openid 并签发 token 的核心代码。// utils/wechat.js const axios require(axios); async function code2Session(code) { const { APPID, SECRET } process.env; const url https://api.weixin.qq.com/sns/jscode2session; const res await axios.get(url, { params: { appid: APPID, secret: SECRET, js_code: code, grant_type: authorization_code } }); if (res.data.errcode) { throw new Error(微信登录失败: ${res.data.errmsg}); } return { openid: res.data.openid, sessionKey: res.data.session_key }; } module.exports { code2Session };参数说明APPID 和 SECRET 放在环境变量里不要硬编码。session_key 用于解密用户手机号等敏感信息不要返回给前端。拿到 openid 后在数据库里查用户是否存在不存在则创建然后签发一个 JWT 返回给小程序端后续请求带在 header 里。这个流程是所有后续功能的前提先跑通再往下做。3. 预约与排队从下单到叫号的完整链路3.1 预约接口的库存扣减与防超卖预约体检套餐的核心问题是库存扣减。每个套餐每天有可预约人数上限多个用户同时下单时不能超卖。常见做法是用 MySQL 的乐观锁或 Redis 的原子操作。我一般用 Redis 预扣加 MySQL 最终确认的两段式方案。// services/appointmentService.js const redis require(../config/redis); const { Appointment, ExamPackage } require(../models); async function createAppointment(userId, packageId, examDate, timeSlot) { const stockKey stock:${packageId}:${examDate}; // 第一步Redis 预扣库存decr 返回扣减后的值 const remain await redis.decr(stockKey); if (remain 0) { // 库存不足回补 await redis.incr(stockKey); throw new Error(该时段预约已满); } try { // 第二步写 MySQL 订单 const appointment await Appointment.create({ user_id: userId, package_id: packageId, exam_date: examDate, time_slot: timeSlot, status: 0 }); // 第三步分配排队号用 Redis 自增 const queueNo await redis.incr(queue:${examDate}); await appointment.update({ queue_no: queueNo }); return appointment; } catch (err) { // MySQL 写入失败回补 Redis 库存 await redis.incr(stockKey); throw err; } }逻辑说明Redis 的 decr 是原子操作能保证并发下不会超卖。预扣成功后写 MySQL如果 MySQL 失败则回补库存。排队号用 Redis 自增生成保证同一天内唯一且递增。参数上stockKey 按套餐和日期维度设置过期时间设为当天结束避免 Redis 里堆积无用 key。这里有个坑如果 Redis 挂了预扣就失效了所以生产环境 Redis 要做持久化和主从。3.2 实时排队进度的推送方案排队进度要实时但不能让小程序端疯狂轮询。常见做法是 WebSocket 长连接但小程序对 WebSocket 的支持有连接数限制而且医院内网环境复杂。我一般用「短轮询加 Redis 缓存」的方案轮询间隔 5 到 10 秒后端从 Redis 读排队状态响应控制在 50ms 以内用户体验上足够「实时」。// controllers/queueController.js const redis require(../config/redis); async function getQueueProgress(req, res) { const { appointmentId } req.params; // 从 Redis 读当前排队信息 const queueInfo await redis.hgetall(queue:info:${appointmentId}); if (!queueInfo || !queueInfo.deptCode) { return res.json({ code: 0, data: null, msg: 暂无排队信息 }); } // 计算前面还有几人 const currentNo parseInt(queueInfo.currentNo, 10); const myNo parseInt(queueInfo.myNo, 10); const ahead Math.max(0, myNo - currentNo - 1); res.json({ code: 0, data: { deptName: queueInfo.deptName, myNo, currentNo, ahead, estimatedMinutes: ahead * 3 // 按每人3分钟估算 } }); }参数说明queue:info:{appointmentId} 是一个 Redis hash字段包括 deptCode、deptName、myNo、currentNo。currentNo 由医生端叫号时更新。estimatedMinutes 是粗略估算实际中可以根据科室历史平均耗时动态调整。这个接口要做缓存控制响应头加 Cache-Control: no-cache避免小程序端拿到旧数据。3.3 医生端叫号与状态同步医生端叫号后要同时更新 Redis 和 MySQL并触发小程序端的消息通知。微信订阅消息需要用户提前授权模板消息现在限制较多常见做法是在小程序内用轮询感知状态变化同时发一条订阅消息作为补充。// services/queueService.js async function callNext(deptCode, examDate) { const currentKey dept:current:${deptCode}:${examDate}; const newNo await redis.incr(currentKey); // 更新该科室下所有等待用户的排队信息 const waitingList await redis.lrange(dept:waiting:${deptCode}:${examDate}, 0, -1); for (const appointmentId of waitingList) { await redis.hset(queue:info:${appointmentId}, currentNo, newNo); } // 持久化到 MySQL await QueueRecord.update( { status: 1 }, { where: { dept_code: deptCode, queue_no: newNo } } ); return newNo; }逻辑说明每次叫号科室当前号自增然后遍历等待列表更新每个用户的 currentNo。这里用 Redis list 存等待队列lrange 取出所有等待中的 appointmentId。实际中如果等待人数很多遍历会有性能问题可以改成只更新当前号前端自己算差值。这个取舍看机构规模日体检量 200 人以下用遍历没问题。4. 报告、档案与健康数据分析让数据产生复利4.1 电子报告生成与 PDF 存储体检报告通常由各科室的检查结果汇总而成最终生成一份 PDF。常见做法是后端用模板引擎渲染 HTML再用 Puppeteer 或 wkhtmltopdf 转 PDF。报告生成后上传到对象存储数据库只存 URL。// services/reportService.js const puppeteer require(puppeteer); const { uploadToOSS } require(../utils/oss); async function generateReport(appointmentId) { const data await getReportData(appointmentId); // 聚合各科室结果 const html renderTemplate(report.html, data); // 模板渲染 const browser await puppeteer.launch({ args: [--no-sandbox] }); const page await browser.newPage(); await page.setContent(html, { waitUntil: networkidle0 }); const pdfBuffer await page.pdf({ format: A4, printBackground: true }); await browser.close(); const url await uploadToOSS(pdfBuffer, reports/${appointmentId}.pdf); await ExamReport.create({ appointment_id: appointmentId, report_url: url, summary: data.summary, is_abnormal: data.hasAbnormal ? 1 : 0, published_at: new Date() }); return url; }参数说明puppeteer.launch 的 --no-sandbox 参数在容器环境里必须加否则会报权限错误。page.pdf 的 format 设为 A4printBackground 设为 true 保证背景色打印出来。报告生成是异步任务不要放在接口请求里同步执行用消息队列或定时任务触发。生成失败要有重试机制重试三次后标记为失败并通知管理员。4.2 健康档案的指标抽取与趋势分析健康档案的价值在于把每次体检的关键指标抽出来形成时间序列用户能看到自己的指标变化趋势。指标抽取在报告生成时同步完成存到 health_archive 表的 indicators 字段。// services/archiveService.js async function extractIndicators(reportData) { // 定义需要追踪的指标及其在报告中的位置 const trackedItems [ { key: systolic, name: 收缩压, unit: mmHg, normalMax: 140 }, { key: diastolic, name: 舒张压, unit: mmHg, normalMax: 90 }, { key: glucose, name: 空腹血糖, unit: mmol/L, normalMax: 6.1 }, { key: cholesterol, name: 总胆固醇, unit: mmol/L, normalMax: 5.2 }, { key: bmi, name: BMI, unit: , normalMax: 24 } ]; const indicators {}; for (const item of trackedItems) { const value reportData.items.find(i i.key item.key); if (value) { indicators[item.key] { name: item.name, value: parseFloat(value.result), unit: item.unit, normal: parseFloat(value.result) item.normalMax, date: reportData.examDate }; } } return indicators; }逻辑说明trackedItems 定义了要追踪的指标白名单避免把所有检查项都塞进档案导致数据膨胀。normal 字段在抽取时就判断好前端直接根据这个字段标红或标绿。趋势分析接口按用户维度查 health_archive把同一指标的多次记录按时间排序返回前端用折线图展示。这里注意单位统一不同医院的报告单位可能不同抽取时要做好换算。4.3 医生在线咨询的消息模型在线咨询不是实时聊天体检场景下更多是用户针对报告提问医生异步回复。用消息表加未读计数就够了不需要上 WebSocket。CREATE TABLE consult_message ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, doctor_id INT NOT NULL, report_id INT COMMENT 关联报告可为空, content TEXT NOT NULL, sender_type TINYINT NOT NULL COMMENT 1用户 2医生, is_read TINYINT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_doctor (user_id, doctor_id) );用户发消息后医生端拉取未读列表回复后用户端轮询或通过订阅消息感知。这个模型简单可靠适合体检这种低频咨询场景。如果要做成实时聊天再引入 WebSocket 或第三方 IM SDK但初期没必要。5. 避坑与排查那些让我返工三次的问题5.1 微信登录 session_key 过期导致解密失败现象用户授权手机号时后端解密报错提示 invalid signature 或解密结果为空。原因session_key 有有效期用户长时间未操作后小程序端持有的 code 已失效但前端没有重新调用 wx.login。解决在解密失败时前端捕获错误码重新执行 wx.login 获取新 code再调一次后端接口。后端不要缓存 session_key每次解密都用最新的。5.2 排队号在跨天后重复现象第二天体检的用户排队号从 1 开始但 Redis 里的 queue:{examDate} key 没有过期导致号段接续。原因Redis key 的过期时间设置成了固定 24 小时而不是当天结束。解决用 Redis 的 EXPIREAT 命令把过期时间设为体检日期次日凌晨 2 点。或者每天定时任务清理前一天的 key。这个坑我在两个项目里都踩过血泪经验是所有按天维度的 key 都要显式设置过期时间不要依赖默认行为。5.3 报告 PDF 生成超时导致接口 504现象用户点击查看报告接口等待超过 30 秒后网关返回 504。原因Puppeteer 启动浏览器实例耗时较长且报告数据聚合涉及多次数据库查询同步执行必然超时。解决报告生成改为异步任务用户点击查看时如果报告未生成返回「生成中」状态前端轮询。生成完成后写入数据库并更新状态。同时 Puppeteer 实例要复用不要每次请求都 launch 一个新浏览器。5.4 健康档案指标单位不统一现象用户在不同分院体检血糖值一个显示 5.6一个显示 100趋势图直接乱掉。原因不同医院的检验科使用的单位不同有的用 mmol/L有的用 mg/dL。解决在指标抽取层做单位归一化定义每个指标的标准单位非标准单位的数值在入库前换算。换算系数要可配置不要硬编码在代码里。这个问题的教训是健康数据涉及单位的地方一定要在数据入口做标准化不要等到展示层再处理。5.5 小程序端轮询导致后端压力过大现象体检高峰期排队进度接口 QPS 飙升Redis 连接数打满。原因每个在线用户每 5 秒轮询一次1000 个在线用户就是 200 QPS虽然 Redis 扛得住但应用服务器的连接池被打满。解决轮询间隔根据排队位置动态调整前面还有 10 人以上时 10 秒轮询一次少于 3 人时 3 秒一次。同时在 Nginx 层对排队接口做限流单 IP 每秒不超过 2 次。这个优化做完后高峰期 CPU 从 80% 降到 30%。6. 进阶技巧用数据埋点验证体检流程数字化的真实效果系统上线不是终点能不能量化「体检流程数字化」带来的提升决定了这个项目在机构内部能不能持续拿到资源。我一般会在关键节点埋三类数据预约转化率、平均排队时长、报告查阅率。预约转化率看的是从进入套餐详情页到完成支付的比例低于 30% 说明套餐展示或价格有问题。平均排队时长从用户签到开始到最后一个科室完成计算对比纸质流程能直观看到提升。报告查阅率是报告发布后 7 天内用户打开 PDF 的比例这个指标低说明推送没做到位。埋点数据用一张简单的日志表存不要上复杂的分析平台。CREATE TABLE event_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT, event_type VARCHAR(50) NOT NULL COMMENT 事件类型, event_data JSON COMMENT 事件参数, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_type_time (event_type, created_at) );event_type 定义几个关键值package_view、appointment_create、checkin、dept_finish、report_view。event_data 里存套餐 ID、科室编码、耗时等上下文。每周跑一次聚合查询把结果发给机构运营方。这个做法看起来简单但实际中很多团队不做导致系统上线后说不清价值。还有一个技巧是排队时长的动态预估。初期用固定值比如每人 3 分钟运行一个月后用 event_log 里 dept_finish 和 checkin 的时间差算出每个科室的实际平均耗时替换掉固定值。这个改动不需要改代码把预估值做成配置项运营后台可调。我自己的习惯是任何估算逻辑都要留一个配置入口不要写死在代码里否则每次调整都要发版。最后说一个我踩过的坑体检注意事项提醒不要做成一次性推送。用户预约后、体检前一天、体检当天早上这三个时间点各推一次内容要有差异。预约后推的是「空腹、带身份证」这类通用提醒前一天推的是「确认预约时间和地点」当天早上推的是「排队号已生成请按时到达」。这个细节看起来小但能显著降低爽约率。希望帮到你。本文还有配套的精品资源点击获取