资讯动态

医院智能挂号系统:小程序+Servlet+MySQL与语音科室推荐

发布时间:2026/10/29 12:15:37 来源:尧图企业网站定制
简介医院智能挂号系统设计和实现的完整PDF文档面向医疗信息化研发人员、系统架构师及高校软件工程相关专业师生针对传统挂号流程效率低、特殊人群操作难等问题给出了系统化解决方案。文档基于实际科研项目详细阐述手动预约、语音输入预约、图形预约三种挂号方式并涵盖症状科普、看病笔记、看病流程引导、模拟取号与模拟诊断等配套模块可用于指导智能医疗系统的设计与开发。资源包仅含1个PDF文件总大小1.88MB文档结构完整附有系统框架、小程序端异步请求、Java项目结构及Servlet类图等关键设计图示。目前已有99人学习浏览内容既涉及自然语言理解、语音识别、高并发后台等核心技术难点也包含与医院信息系统集成及移动支付对接的考虑可为毕业设计、课程项目或医疗信息化改造提供直接参考与代码实现思路。1. 医院智能挂号系统不只是把挂号搬到网上医院智能挂号系统的难点从来不在“挂号”本身而在面对老人、儿童、视障用户这类特殊群体时交互怎么设计、语音怎么识别、科室怎么推荐。这篇论文给出的方案是把手动选科、语音输入、图形挂科、症状科普四种方式放进同一个微信小程序里前端用小程序原生框架后端走阿里云服务器上的 Java Servlet数据库落在腾讯云 MySQL。它是三类技术人员的参考范本做小程序开发的可以看前后端异步通信怎么写做后端系统设计的可以参考 Servlet 按 mission 参数分发业务类的思路做智能系统产品的可以借鉴多入口交互如何兜住不同人群。如果你正准备开发类似的挂号、导诊或预约类系统这篇论文相当于一份完整的需求说明加技术选型清单直接照着他的框架落地即可。2. 系统架构拆解小程序、Servlet、MySQL 三者如何串起来2.1 前后端分离的轻量方案为什么选小程序加 Servlet整个系统分三段微信小程序负责页面展示和用户交互Java Servlet 作为后端接口层处理业务逻辑腾讯云 MySQL 存科室、用户、预约记录。小程序端的架构和 Html5 那一套很像wxml 加 wxss 控制布局js 控制事件。后端没有用 Spring Boot 这类重型框架而是直接落到 Tomcat 上的 JavaEE Servlet原因很简单——学生项目、功能相对集中Servlet 足够用部署也省心一个 war 包丢进 Tomcat 的 webapps 就能跑。这种选型给从业者的参考价值在于不是所有系统都得上微服务业务边界清楚、并发量可控的场景Servlet 加单库 MySQL 反而更容易维护。原文里提到一个很关键的设计——小程序发送异步请求时带一个 mission 参数Servlet 端根据这个参数决定调用哪个业务类。这相当于一个最简版的路由分发器避免了为每个功能单独开一个 Servlet 的冗余写法。// 小程序端发送异步请求的核心代码 wx.request({ url: http://服务器IP:8080/hospital/WxServlet, data: { mission: yuyue, // 业务标识yuyue预约quxiao取消paidui排队 openid: userInfo.openid, // 用户唯一标识从小程序登录态获取 keshi: selectedDept, // 用户选中的科室名称 yuyueTime: currentTime // 预约时间戳 }, method: POST, success(res) { // res.data 是后端返回的 JSON 字符串 // 通过 fastjson 解析后渲染到页面 if (res.data.code 200) { wx.showToast({ title: 预约成功 }); } } });这段代码的关键在 mission 参数的设计。所有接口共用同一个 WxServlet通过 mission 值区分业务类型好处是前端只需要维护一个请求地址坏处是 mission 值一旦拼错后端会找不到对应的业务分支所以建议在项目里维护一个常量类统一管理 mission 的取值避免魔法字符串散落在各个页面。openid 从 wx.login 的 code 换出来是微信生态里识别用户身份的标准做法后端存用户表时用 openid 做唯一索引别用自增 id。2.2 后端 mission 分发机制与业务类划分后端 WxServlet 收到请求后第一步不是处理业务而是解析 mission 参数决定路由方向。原文提到的 MissionSelec 类就是干这件事的。不同业务类各司其职负责预约的叫 Yuyue 类负责科室加载的叫 secion 类负责语音病情分析的是 Diagnosis 类。每个类对应数据库的一张或几张表类的成员方法对应增删改查操作。// WxServlet 核心分发逻辑简化版 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String mission request.getParameter(mission); JSONObject result new JSONObject(); switch (mission) { case loadKeshi: // 调用 secion.java 从数据库读取科室列表 ListDept deptList new secion().getAllDepts(); result.put(data, deptList); break; case yuyue: // 调用 Yuyue 类的预约方法写入预约表 new Yuyue().addYuyue(request.getParameter(openid), request.getParameter(keshi)); result.put(code, 200); break; case quxiao: new Yuyue().cancelYuyue(request.getParameter(openid)); break; } // 用 fastjson 统一打包返回 response.setContentType(application/json;charsetUTF-8); response.getWriter().write(result.toJSONString()); }这里有个实践中常踩的坑Servlet 默认的编码不是 UTF-8如果不在 doPost 开头设置 request 和 response 的字符编码中文科室名存进 MySQL 再查出来就是一串问号。原文没提这个细节但做 Java Web 的人应该都有过这段血泪经历。另外 fastjson 打包返回的字段名要和前端 js 里取数据的字段名严格一致比如 Java 端叫 data前端就用 res.data大小写差一个字母都是 undefined。2.3 数据库表设计与预约状态的核心约束系统涉及的核心表有科室表、个人信息表、预约表、排队表。预约表是流转的枢纽用户在线上预约后记录先在预约表里点击模拟取号时把记录从预约表搬到排队表模拟诊断完毕后再从排队表删除并写入历史挂号表。这个设计对做排队叫号类系统很有参考价值——用状态转移代替在一张表里堆字段每张表只负责一个生命周期阶段数据更干净排查问题时也更容易定位。表名关键字段作用keshi 科室表id, name, icon存储科室名称与图标前端宫格渲染的数据源userinfo 个人信息表openid, name, phone用户身份信息openid 做唯一键yuyue 预约表id, openid, keshi, time线上预约记录取号前存在这里paidui 排队表id, openid, paihao, status线下排队记录模拟取号后转入history 历史挂号表id, openid, keshi, time, status诊断结束后归档供历史记录页查询预约表和个人信息表之间通过 openid 关联不需要外键约束业务层控制逻辑即可。这里一个值得借鉴的设计是“一次只允许一个有效预约”——用户想换科室必须先把原来的预约取消掉。这种硬约束避免了同一用户重复占号的问题实现上也很简单预约前先查该 openid 在预约表里是否有 status 为有效的记录有则提示先取消。3. 三大挂号方式的实现手动、语音、图形背后的共同逻辑3.1 手动选科宫格渲染与预约状态的回显逻辑手动选科是系统的主入口页面加载时通过 onload 方法向后端请求科室列表后端从 MySQL 查出数据后用 fastjson 打包成 JSON 数组返回前端拿到数据后用一个 for 循环渲染成宫格每行固定三个科室这是通过 CSS 控制宽度实现的。这一个功能虽然简单但背后有两处交互细节值得注意一是已预约用户的回显提醒二是预约与取消的状态互斥。// 页面加载时检查用户是否已有预约 Page({ onLoad: function () { let that this; wx.request({ url: BASE_URL /WxServlet, data: { mission: checkYuyue, openid: app.globalData.openid }, success(res) { if (res.data.hasYuyue) { that.setData({ hasYuyue: true, yuyueInfo: res.data.yuyueInfo // 已有预约的科室和时间 }); wx.showModal({ title: 提示, content: 您已预约 res.data.yuyueInfo.keshi 是否先取消再重新预约, confirmText: 去取消, success(res) { if (res.confirm) { that.cancelYuyue(); } } }); } } }); } });这段逻辑防止了一个常见问题用户重复预约导致数据库里出现多条有效预约记录。很多初做这个功能的人会漏掉 onload 时的状态检查只在点击预约时校验结果用户从别的入口进入时依然能重复提交。另一个细节是宫格的每行三个科室这个约束应该由前端 CSS 控制而不是后端返回时分组因为屏幕宽度是前端属性后端不需要关心渲染细节。3.2 语音输入WechatSI 转文字加 HanLP 推荐科室语音挂号是面向老人和视障用户的核心功能技术链路分两段前端用微信官方同声传译插件 WechatSI 把语音转成文字后端用 HanLP 对文字做自然语言处理匹配出最接近的科室。这个方案巧妙的地方在于没有自己训练语音识别模型直接复用微信生态的插件能力——在 mp 后台申请插件权限前端引入插件长按按钮录音插件返回识别文本再把这个文本作为病情描述发送给后端。HanLP 的接入方式是重点。文章诊断类病情的流程是读取服务器上保存的字典和语法语义模型对病情描述做分词、词性标注再通过推荐算法计算每个科室的匹配度返回匹配度最高的科室。HanLP 本身支持自定义词典这个场景下需要准备一份症状词到科室的映射字典比如“头疼”“头晕”“失眠”映射到神经内科“咳嗽”“发烧”“咽痛”映射到呼吸内科。// Diagnosis.java 科室推荐的核心逻辑简化示例 public String recommendKeshi(String description) { // 加载 HanLP 自定义症状词典 HanLP.Config.CustomDictionaryPath new String[]{data/symptom_dict.txt}; // 对用户描述做分词 ListTerm termList HanLP.segment(description); // 统计每个科室命中的症状词数量 MapString, Integer keshiScore new HashMap(); for (Term term : termList) { String word term.word; // 从症状-科室映射表中查找该词对应的科室 String keshi symptomToKeshi.get(word); if (keshi ! null) { keshiScore.merge(keshi, 1, Integer::sum); } } // 按得分排序返回最高分科室 return keshiScore.entrySet().stream() .max(Map.Entry.comparingByValue()) .get().getKey(); }这段代码里最容易翻车的地方是自定义词典的路径。HanLP 默认从 classpath 根目录加载词典如果词典文件放在 data 子目录下路径就得写成 data/symptom_dict.txt写错路径会导致分词结果完全没命中自定义词。另一个坑是词典的编码格式必须是 UTF-8用记事本另存的 ANSI 编码会导致乱码词条。推荐算法这里用的是比较朴素的词频统计实际项目中可以升级成 TF-IDF 或 BM25原理一样效果会好不少。3.3 图形挂科人体切块与部位到科室的映射图形挂科是把一张人体图切成头部、胸部、腹部、四肢等区块用户点击某个部位后系统列出该部位对应的常见科室再跳转到手动挂科界面完成预约。这个功能在视觉上看着复杂实现上其实就是 image 组件的点击区域划分加上一张部位-科室映射表。切图有个不那么显然的细节不能直接用一张整图做背景然后硬编码坐标因为不同手机屏幕的适配会出问题。原文提到用切图工具把人体图切成多个小块视觉上通过代码属性让它们衔接美观这就绕开了坐标适配问题——每个部位是一张独立的小图用 view 组件并排或叠加摆放点击事件绑定在每张图上天然适配不同屏幕。映射表的设计也要留个心眼。一个部位可能对应多个科室比如头部既可能是神经内科头疼也可能是耳鼻喉科耳鸣还可能眼科视力模糊。映射表应该设计成一对多结构而不是简单的字符串字段。这里的推荐思路是在映射表中给每个科室加权重比如点击头部时“神经内科”权重最高排第一但用户仍可以滑动查看其他科室保留选择余地。4. 状态流转的设计预约、取号、诊断全流程如何用一张表驱动4.1 看病流程图的状态机设计系统用 iView 的步骤图组件展示看病流程全局变量 app.globalData.current 控制当前所处步骤。每次操作让 current 增减线上预约完成进入步骤2模拟取号进入步骤3模拟诊断完毕进入步骤4。主页面每次 onshow 时重载这个参数保证流程图与用户实际操作进度一致。// 全局步骤状态管理 App({ globalData: { current: 1, // 1选择科室, 2预约成功, 3已取号排队, 4诊断完毕 openid: null, yuyueInfo: null }, onLaunch() { // 从缓存恢复登录态 let openid wx.getStorageSync(openid); if (openid) { this.globalData.openid openid; wx.setStorageSync(current, this.globalData.current); } } }); // 模拟取号时更新步骤 function simulateQuHao() { app.globalData.current 3; wx.setStorageSync(current, 3); // 调用后端把预约记录转移到排队表 wx.request({ url: BASE_URL /WxServlet, data: { mission: quhao, openid: app.globalData.openid }, success(res) { if (res.data.paihaoNum) { that.setData({ paihaoNum: res.data.paihaoNum }); } } }); }这里的一个实战经验步骤状态必须持久化不能只存在内存里。小程序进程可能被系统回收用户切后台再回来内存里的 globalData 可能已经丢失。最简单的方式是同步到 Storage每次启动时读取每次变更时写入。后端也可以存一个状态字段但前端本地持久化能保证页面渲染不错乱后端状态则用于保证数据一致性。4.2 模拟取号的数据迁移操作模拟取号是连接线上预约和线下排队的桥梁核心操作是把用户预约记录从预约表转移到排队表同时删除预约表中的原记录并且返回一个排队号码。这个操作用到了事务必须保证迁移和删除同时成功或同时失败否则会出现用户既在预约表中又在排队表中的重复状态。-- 模拟取号的数据迁移操作在业务类中通过事务控制 -- Step1: 从预约表查询用户的有效预约 SELECT * FROM yuyue WHERE openid #{openid} AND status 1; -- Step2: 插入排队记录并生成排队号 INSERT INTO paidui (openid, keshi, paihao, status) VALUES (#{openid}, #{keshi}, #{maxPaihao 1}, 1); -- Step3: 删除预约表中的原记录 DELETE FROM yuyue WHERE id #{yuyueId};三个步骤必须放在同一个事务里。另外排队号的生成不能简单地用当前最大排队号加一因为高并发下两个请求同时读到同一个最大值会生成重复排队号。常见做法是用数据库的自增主键做排队号或者用 Redis 的 INCR 命令生成递增序列。这个系统因为是模拟场景用户量很小用 max 加一问题不大但如果真要做线上系统这一处必须改。4.3 模拟诊断完毕与历史记录归档模拟诊断完毕是这个流程的收尾动作用户完成取号后点击按钮后端把用户从排队表移除同时将这次挂号信息写入历史表。这样设计的好处是业务表始终保持轻量——排队表只有当前在排队的人历史表记录所有已完成的挂号查询历史时单独查归档表不会因数据量增长拖慢主流程查询。// 模拟诊断完毕的归档操作 public void finishDiagnose(String openid) { // 开启事务 Connection conn getConnection(); conn.setAutoCommit(false); try { // 查询排队信息 String sql SELECT * FROM paidui WHERE openid? AND status1; PaiduiRecord record query(sql, openid); // 1. 写入历史表 String insertHistory INSERT INTO history (openid, keshi, paihao, finish_time) VALUES (?, ?, ?, NOW()); execute(conn, insertHistory, openid, record.getKeshi(), record.getPaihao()); // 2. 删除排队记录 String deletePaidui DELETE FROM paidui WHERE id?; execute(conn, deletePaidui, record.getId()); // 3. 提交事务 conn.commit(); } catch (Exception e) { conn.rollback(); throw e; // 需要保证归档与删除的原子性 } }这段逻辑里有个容易被忽视的点归档操作必须记录完成时间历史记录页面需要按时间倒序展示。如果历史表里没有时间字段用户看到的多条记录无法区分先后顺序体验会打折扣。另外历史表和预约表的结构几乎一样很多人会直接复用预约表加一个 status 字段来区分是否完成但这样会让主表越来越臃肿建议还是按状态拆表归档后记录只读不再参与业务流转。5. 避坑指南从论文到线上系统最容易翻车的六个细节5.1 一次只能预约一个科室的限制现象用户预约了神经内科后又想预约呼吸内科系统直接提示失败没有给出取消入口。原因后端在预约接口做了硬校验——预约表中存在该 openid 的有效记录时直接返回错误码但前端没有在错误码返回时引导用户跳转到取消预约页面。解决前端收到“已有有效预约”的错误码后弹出提示框提供“去取消”按钮点击后跳转到历史预约页面先执行取消操作。后端校验保留前端引导补齐两层配合才能形成闭环。实际开发中建议后端返回的错误码要包含一个 action 字段告诉前端下一步该做什么。5.2 预约人数和排队号码的数据不一致现象页面上显示的预约人数还是预约前的数字用户点击模拟取号后拿到排队号码但页面上的人数没有刷新。原因模拟取号成功后前端只处理了排队号码的回显没有重新请求接口刷新预约人数和科室状态。解决在模拟取号的 success 回调里除了显示排队号再发一个请求拉取最新的科室预约数据或者让后端在一次返回里把排队号和最新人数一起打包返回减少一次网络往返。这种问题在多模块联调时很常见解决方法就是每次状态变更后刷新所有依赖该状态的 UI 数据。5.3 中文乱码问题现象科室名称从数据库查出来再到前端显示变成了一串问号。原因三层传输中有一层字符编码不一致。MySQL 表结构是 utf8 编码但 JDBC 连接串没有指定 characterEncoding或者 Servlet 的 response 没有设置 UTF-8。解决JDBC 连接串加上 characterEncodingutf-8Servlet 的 request 和 response 都设置 setCharacterEncoding(UTF-8)同时 MySQL 建表时统一用 utf8mb4——注意 utf8mb4 和 utf8 的区别utf8mb4 才能完整支持 emoji 和生僻字。字符编码这个问题往往不是难在解决而是难在排查所以建议在项目初始化时就把编码配置统一列好别等出了问题再满项目找。5.4 语音识别准入门槛与实际效果落差现象使用 WechatSI 插件做语音转文字识别结果和用户实际说的内容偏差很大尤其是带方言口音的用户。原因WechatSI 是通用语音识别对普通话识别率高但对方言支持有限而且“头疼”“脑壳痛”这类同义表达在不同的地区差异巨大。解决在 HanLP 的症状词典里把方言词和普通话表达做同义映射比如“脑壳痛”“头痛”“脑袋疼”都映射到同一个症状词再对应到神经内科。词典的维护不是一次性工作上线后要持续收集用户的真实表达定期补充词条。另外识别结果返回后可以在前端做一次二次确认把识别出的文字显示给用户看用户点击确认后再发送给后端避免误识别直接产生错误的科室推荐。5.5 小程序真机预览与开发者工具差异现象开发者在工具里跑得好好的真机预览时某些页面空白或样式错乱。原因开发者工具的模拟器和真机渲染引擎不完全一致部分 CSS 属性和组件在真机上表现不同。最常见的是 flex 布局的兼容问题以及部分组件如 picker 在工具里弹层正常真机却定位错乱。解决养成真机预览的习惯尤其是涉及布局和交互的功能不能只依赖开发者工具。出现差异时优先检查 CSS 兼容性用开发者工具的“机型模拟”多切换几种机型测试。另外请求的域名必须是 HTTPS 且在微信公众平台后台配置过合法域名开发者工具可以关掉校验真机不行这个问题会直接导致所有请求失败页面数据加载不出来。5.6 HanLP 词典加载失败导致推荐接口报错现象语音输入后后端报错“词典加载失败”或“找不到文件 data/symptom_dict.txt”。原因路径配置错误或文件缺失。HanLP 默认从 classpath 根目录查找打包成 war 包后文件路径可能和本地开发不一致。解决将自定义词典放在项目的 resources 目录下打包后会自动进入 classpath路径写成 classpath:data/symptom_dict.txt。调试阶段可以通过 HanLP.Config.ShowTermNature 开启分词过程日志直接看到词典是否加载成功。这里给个排查建议先写一个最简单的 main 方法测试 HanLP 分词确认词典加载没问题再接入 Servlet 业务逻辑缩小排查范围。这一步能帮你分清是 HanLP 本身的问题还是业务代码的问题。6. 把它做成能上线的系统四个值得改造的点和一条验证路径6.1 从单 for 循环到科室分组渲染原文明确提了一个待改进点科室列表没有分组不管内科外科儿科都用一个 for 循环平铺渲染。小科室少看不出问题三甲医院动辄几十个科室用户滑动查找的成本极高。改造思路是给科室表加一个 parent_id 字段大类为 0小类指向父级 id前端先渲染大类点击后再加载该大类下的子科室用二级联动替代平铺列表。这样做并不复杂后端多一个按 parentId 查询的接口前端用两个数组分别存大类和小类交互上参考市面上的地图选点组件。6.2 从模拟排队到实时排队状态推送这个系统里的取号和诊断都是模拟操作做演示没问题真实场景需要的是实时性。用户在线上预约后到医院现场用叫号机扫码取号排队号码和前方等待人数要实时可见。改造路径有两条第一条是短轮询前端每隔 10 秒调一次接口查排队状态简单但浪费请求第二条是 WebSocket 长连接后端把排队状态变化推给对应 openid 的小程序端。微信小程序的 WebSocket 接口叫 wx.connectSocket服务器端可以用 Tomcat 的 WebSocket 支持或单独部署一个推送服务。对于这套基于 Servlet 的系统建议先用短轮询逻辑改动小上线跑通后再换长连接。6.3 从规则推荐到基于历史挂号的协同过滤原文提到了一个思路但没展开根据用户历史挂号记录做科室推荐。比如一个用户每次都挂呼吸内科再来挂号时可以在页面上优先展示呼吸内科。实现上分两层轻量版是统计该用户历史挂号中出现次数最多的科室直接排序靠前进阶版是算科室之间的关联性——挂了五官科的用户有大概率也挂过呼吸内科用协同过滤的思路计算“看了A科室的人还看了B科室”把关联科室推荐给新用户。数据量不大时用 SQL 直接算就行用户多了再考虑离线计算。6.4 让云笔记本成为真正的电子病历卡原文里的看病笔记目前只做了记录功能也就是一个本地备忘。要变成电子病历卡需要加上时间和随访两个维度每次挂号时自动生成一条时间戳用户可以在时间戳下补充当次诊断结果和用药信息复查时系统提示“您上次是 3 个月前看的呼吸内科建议复查”。这里的关键是数据结构——每条笔记要挂上挂号记录的 id否则笔记和看病历史是割裂的。我用过不少医院的 App做得差的病历卡就是两个模块各玩各的就诊记录归就诊记录备忘录归备忘录根本没法形成时间线。6.5 完整验证路径从项目启动到真机走通全流程拿到这套源码或文档后不要东看一眼西看一眼按下面的顺序验证一次准备环境JDK 8、Tomcat 8.5、MySQL 5.7、微信开发者工具导入数据库脚本确认科室表和用户表里有测试数据后端项目打成 war 包丢进 Tomcat 的 webapps启动 Tomcat浏览器访问 Servlet 接口地址确认能返回 JSON 数据微信开发者工具导入小程序端代码把请求地址改成服务器局域网 IP完整走一遍流程登录 → 手动选科 → 预约 → 模拟取号 → 模拟诊断完毕 → 查历史记录语音输入功能测试长按录音确认识别文字正确后端返回科室推荐我自己的习惯是拿到一个新的前后端联调项目先写一个最简的测试页面把登录态跑通再做业务功能验证。因为登录态是整个系统的前置条件登录有问题后面全是白搭。语音识别和 HanLP 推荐这两个模块单独写测试类验证不要在页面上调试那样你分不清是语音没转出来还是 HanLP 匹配错了还是前端渲染挂了。最后说一句这类智能挂号系统的核心从来不是某个单点技术有多深而是手动、语音、图形三种入口如何自然地服务于不同人群以及预约、排队、诊断这条状态链怎么保持数据一致。建议你先把状态流转的数据表关系彻底吃透再动手改代码顺序反了容易被细节带偏。希望这篇拆解能让你在复现时少走几个弯路有问题可以对照原文的参考文献目录按图索骥那里面的资料都是可以直接落地的。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑