简介:这份PDF文档完整呈现了医院智能挂号系统的设计与实现方案,适合医疗信息化开发者、软件工程专业学生以及人工智能与系统开发方向的学习者参考。文档从患者实际使用场景出发,系统梳理了手动选科、语音输入、图形选科、症状科普、看病笔记、看病流程、模拟取号与模拟诊断等核心功能,并详述了小程序前端、腾讯云MySQL数据库及阿里云服务器上Tomcat+JavaEE+Servlet后端的整体系统框架。针对语音预约中的自然语言理解与识别、高并发挂号请求下的数据库与服务器稳定性、用户界面无障碍设计等关键技术难点,给出了面向实际落地的设计考虑与实现思路。资源为单个PDF文件,共1.88MB,无需解压即可直接阅读;目前已有99人学习,适合用来快速了解智能挂号系统的完整架构与实现路径。
1. 医院智能挂号系统:当挂号遇到 NLP 和微信小程序
去过医院的人都知道,挂号窗口排队一小时、看病五分钟是常态。这篇论文设计了一个微信小程序端的智能挂号系统,核心思路不是简单地把线下挂号搬到线上,而是针对不同人群提供了三条挂号路径:手动选科、语音输入、图形挂号。语音输入依赖 HanLP 做意图识别,把“我头疼咳嗽”这类自然语言映射到对应科室;图形挂号则是点人体部位图,比如点“眼睛”直接跳到眼科。系统后端是阿里云服务器上的 JavaEE + Servlet,数据库用了腾讯云 MySQL,前端是微信小程序。
这个设计的价值在于照顾了老人、儿童、残障人士这类使用传统挂号系统有困难的人群,而且整套代码结构清晰,是学习微信小程序 + Java 后端 + NLP 落地的一个不错参考。如果你是做医疗信息化或者想了解语音交互在垂直场景落地的开发者,这份资料值得下下来看看。
2. 系统框架与技术选型:小程序端、Java 后端、MySQL 三方联动
2.1 整体架构:为什么选微信小程序而不是 App
这个系统的架构是典型的前后端分离模式,但前端选择了微信小程序而不是原生 App 或 H5。原因很直接:小程序免安装、传播方便,患者扫个码就能用,尤其适合医院这种低频但刚需的场景。骨架是 wxml + wxss 控制页面布局,js 控制事件逻辑,整体结构类似熟悉的 HTML5 + CSS + JS。
从项目结构来看,小程序端和后端通过异步请求互相通信。用户在页面触发一个操作,比如点击“手动选科”,小程序端的 JS 就会调用 wx.request 发给部署在阿里云上的 Java Servlet,Servlet 再根据请求参数里的 mission 字段决定调用哪个业务类,最后把数据库操作结果打包成 JSON 返回给前端渲染。
2.2 Java 后端与数据库表设计
后端用的是 JavaEE + Servlet,没有引入 Spring 全家桶,而是保持了最原生的 Servlet 写法。项目结构里 WxServlet 是统一的入口类,MissonSelec 根据 mission 参数做分发。这种设计虽然看起来简陋,但对于中小型医院挂号这种并发量不算极高的场景,原生 Servlet 反而更轻量、更直观,特别适合拿来做课程设计或入门练手。
数据库是腾讯云 MySQL,涉及几个核心表。科室表存储科室 id、科室名、诊室位置等信息;个人信息表存患者昵称、手机号、头像;预约表存预约记录,包括用户 id、科室 id、预约时间、状态;排队表则在模拟取号后把预约记录转移过来。原文还提到一个 sectoinguake 表,看名字应该是记录科室对应的排队号码段,用来实现“一次只能预约一个科室,取消后才能约新的”这种控制逻辑。
2.3 异步请求的代码套路:wx.request 与 Servlet 的对应关系
论文里贴了小程序的异步请求示例,这是整个系统通信的关键。前端代码大致是这样的逻辑:
wx.request({ url: 'http://服务器地址/WxServlet', data: { mission: 'querySection', userId: '1001' }, header: { 'content-type': 'application/json' }, success: (res) => { console.log(res.data); // 拿到科室列表后渲染到页面宫格 }, fail: (err) => { console.error('请求失败', err); wx.showToast({ title: '网络异常,请重试', icon: 'none' }); } })这里几个参数要解释一下。url 是后端 Servlet 对外暴露的地址;data 里的 mission 字段很关键,后端靠它区分这次请求是查科室、预约还是取消预约。success 回调里拿到的是后端返回的 JSON 数组,前端再用 setData 更新页面数据。
对应地,后端的 WxServlet 类在 doPost 方法里做分发,核心逻辑大致是:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String mission = request.getParameter("mission"); String result = ""; if ("querySection".equals(mission)) { result = new SectionService().queryAllSections(); } else if ("makeAppointment".equals(mission)) { result = new YuyueService().createAppointment(request); } else if ("cancelAppointment".equals(mission)) { result = new YuyueService().cancelAppointment(request); } else if ("voiceDiagnosis".equals(mission)) { result = new DiagnosisService().diagnoseBySymptom(request); } response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(result); }这里有个容易踩坑的细节:request.getParameter 默认取的是 URL 编码参数,如果前端 wx.request 的 data 是对象形式,默认会序列化成 key=value&key=value 的形式,后端用 getParameter 能直接拿到。但如果有人改成了 JSON 字符串传参,后端就要手动解析 JSON,很多初学者在这个环节翻车。如果你在调试时发现 Servlet 里 getParameter 返回 null,先检查前端 content-type 是不是 application/x-www-form-urlencoded 或默认值。
3. 核心功能逐一拆解:从手动选科到 NLP 语音挂号
3.1 手动选科:宫格渲染与预约去重
手动选科是基础功能,页面加载时通过 onLoad 方法自动向后端发请求,后端调用 SectionService 查询科室表,用 fastjson 把结果打包成 JSON 数组返回。前端拿到数据后用 for 循环渲染宫格,每行固定三个科室,这个可以通过 CSS 的 flex 布局加 flex-wrap 实现,每一格宽度设为 33.33% 就自然三列了。
预约去重的逻辑值得说一下。如果用户已经预约过一个科室,再点新科室时系统会提示先取消旧的预约。这种设计避免了一个人占多个号的情况,实现方式是每次预约前先查数据库里该用户是否已有预约记录。论文里提到“一次只能预约一个科室,如果有需求可以不设限制”,也就是说如果你做课程设计想简化,去掉这个限制也没什么大问题,但从医疗服务真实场景来说,保留会更合理。对应地,后端预约类 Yuyue 里有一个预约方法和一个取消预约方法,预约时先查再插,取消时先删再释放科室号,两个操作写成独立接口分开调用。
3.2 语音输入挂号:WechatSI 转文字 + HanLP 推荐科室
这是整套系统最有技术含量的一块,也是论文标题里“智能”二字的主要落点。语音输入流程分两步:前端用微信官方同声传译插件 WechatSI,长按按钮说话,插件把语音转成文字;拿到文字后发给后端,后端调用 Diagnosis 类,用 HanLP 开源 NLP 工具包里的推荐算法做文本分析,匹配出最合适的科室返回给前端。
先看前端的语音采集,核心代码逻辑是这样的:
const plugin = requirePlugin("WechatSI"); const manager = plugin.getRecordRecognitionManager(); manager.onRecognize = function(res) { // 实时识别结果 console.log(res.result); } manager.onStop = function(res) { // 长按结束,拿到最终识别文本 const text = res.result; wx.request({ url: 'http://服务器地址/WxServlet', data: { mission: 'voiceDiagnosis', symptomText: text }, success: (resp) => { wx.showModal({ title: '推荐科室', content: resp.data.sectionName, success: (confirm) => { if (confirm.confirm) { // 跳转到挂号页面,携带科室 id wx.navigateTo({ url: '/pages/appointment/appointment?sectionId=' + resp.data.sectionId }) } } }) } }) }这里有几个参数需要说明。getRecordRecognitionManager 是 WechatSI 插件提供的语音识别管理对象,onRecognize 在说话过程中持续回调,onStop 在松手时触发并返回最终结果。注意 onStop 返回的 text 才是完整的,onRecognize 里的只是中间临时结果,一般不用来发请求。
后端的 Diagnosis 类是这块的核心,论文里贴了一张图说明原理,特别提到“为了直观展示原理,图 23 中 titleArray 的值不经过读取,而是直接定义”。也就是说,真正实现时要从服务器读取官方提供的字典和语法语义模型,然后调用 HanLP 的推荐算法来做。
Java 端的诊断逻辑代码大致如下:
public String diagnoseBySymptom(String symptomText) { // HanLP 分词 + 关键词提取 List<String> keywords = HanLP.extractKeyword(symptomText, 5); // symptomDict 是从文件加载的 症状 -> 科室 映射字典 Map<String, String> symptomDict = loadSymptomDict(); Map<String, Integer> sectionScore = new HashMap<>(); for (String keyword : keywords) { String section = symptomDict.get(keyword); if (section != null) { sectionScore.put(section, sectionScore.getOrDefault(section, 0) + 1); } } // 取分数最高的科室 String bestSection = sectionScore.entrySet().stream() .max(Map.Entry.comparingByValue()) .map(Map.Entry::getKey) .orElse("内科"); // 兜底科室 return bestSection; }这段代码的逻辑可以这样理解:先用 HanLP 提取用户描述里最关键的几个词,比如“头疼”“咳嗽”“发热”,然后拿着这些词去查预先维护好的症状字典,字典里每个症状词对应一个或几个科室。每个关键字命中就给对应科室加一分,最终得分最高的科室就是推荐结果。这个方案比简单做规则匹配强的地方在于分词能力——HanLP 能把“我最近老咳嗽还发烧”正确地切成“咳嗽”和“发烧”,而不会切出“老咳”这种奇怪组合。
但说实话,这个方案的效果取决于症状字典的质量。论文里是直接读官方提供的词典,实际做的时候你需要自己扩充这份字典,比如“胸痛”对“心内科”和“呼吸内科”、“反酸”对“消化内科”,这种一对多的情况要靠综合评分来排。你可以在字典里给每个症状词加权重,比如“剧烈胸痛”直接给心内科加高分而不是只加一分,这样推荐准确性会好不少。
3.3 图形挂科:人体部位图切片与科室映射
图形挂科面向两类人:听力和文字读写有障碍的,以及根本不知道自己该挂哪个科的。操作方式是把人体模型图切成若干区域,每个区域绑定一组科室,用户点击身体的某个部位,下方就列出对应的常见科室。
论文里提到“视觉上可以通过切图工具和修改代码属性让其衔接得更美观”,也就是说,切图之后你不用精确到每个像素都完美贴合,只要在可接受范围内就可以。实现层面,可以用 image 标签的 map 属性做热区,或者更直接的做法是——把人体图分成头部、胸部、腹部、四肢几个区域,每个区域单独一张小图,用绝对定位放到对应的位置。点击事件绑定到每张小图上。
这个模块的代码逻辑不算复杂:
<view class="body-map"> <image src="/images/body/head.png" bindtap="handleBodyPartTap">handleBodyPartTap(e) { const part = e.currentTarget.dataset.part; const partSectionMap = { head: ['神经内科', '耳鼻喉科', '眼科'], chest: ['心内科', '呼吸内科'], abdomen: ['消化内科', '普外科'], legs: ['骨科', '血管外科'] }; this.setData({ showSections: true, sectionList: partSectionMap[part] || ['内科'] }); }>// app.js 中定义全局变量 globalData: { current: 1 } // 页面中更新步骤 function updateCurrentStep(step) { app.globalData.current = step; this.setData({ currentStep: app.globalData.current }); } // 主页 onShow 时重新加载 onShow: function() { this.setData({ currentStep: app.globalData.current }); }
这个设计的特点在于把状态统一收敛到全局变量,而不是散落在每个页面各自的 data 里。好处是无论用户从哪个入口进来,页面显示的状态都是一致的,不会出现在挂号页显示已完成、回主页却显示未开始的情况。
3.6 模拟取号与模拟诊断:表记录的流转逻辑
模拟取号是系统的一个亮点,它模拟了患者到达医院后从预约状态变成线下排队的过渡。点击“模拟取号”按钮后,后端把预约表里的记录删除,同时写入排队表,并返回一个排队号码给前端。排队表里记录的是当前正在排队的用户,所以科室界面上显示的“当前排队人数”其实就是去查这个表里对应科室的记录数。
核心逻辑是这样的:
-- 模拟取号:开启事务,保证两步操作原子性 BEGIN; -- 从预约表中取出一条记录 -- 并删除该记录 DELETE FROM t_appointment WHERE user_id = '1001' AND section_id = '3'; -- 插入排队表,自动生成排队号 INSERT INTO t_queue (user_id, section_id, queue_number, status) VALUES ('1001', '3', (SELECT next_queue_number('3')), '1'); COMMIT;这条流程要注意事务控制。删除和插入必须是一个事务,不然会出现用户预约没了但也没排上队的中间状态。论文里没有明确提到是否用了事务,但从实际工程角度看这是必要的,你要自己加上。模拟诊断完毕的操作更直接——把用户从排队表中移除,再把这次挂号信息写到历史记录表中,方便后续查看。
这个过程相当于把线下“排队—叫号—就诊”的流程数字化了,每一步操作都会触发数据表的状态流转,而且流转的方向是单向的:预约表进排队表,排队表进历史表。数据库设计上要注意给这些表之间加上必要的关联字段,比如 user_id 和 section_id,不然数据很容易错乱。
4. 避坑指南:开发这套系统的 5 个典型翻车点
4.1 语音识别返回结果为空
现象:长按说话后 onStop 回调触发,但 res.result 是空字符串,页面没有任何反应。
原因:有两种可能,一是 WechatSI 插件需要在小程序后台申请才能调用,没开通权限时插件默默失败;二是用户说话时间太短,识别还没开始就松手了,插件没有捕获到有效语音。
解决:先检查 app.json 里是否配置了插件引用,确认小程序后台的“插件管理”里已经添加了 WechatSI。代码层面做一个保护——如果识别结果为空,用 wx.showToast 提示“未识别到语音,请重试”,而不是直接把空字符串发给后端。
4.2 HanLP 推荐科室不准确
现象:用户说“我胃疼”,推荐结果却是心内科。
原因:症状字典不够完善,或者分词后提取的关键词没有命中字典。HanLP 默认模型对单字词和口语化表达效果一般,“胃疼”可能被分成“胃”和“疼”两个单字,而你的字典里只有“胃痛”没有“胃疼”。
解决:在加载字典时做同义词归一化,把“疼”“痛”“不舒服”这类字眼映射到规范症状词,一个简单的办法是配置一个同义词映射表:
private static final Map<String, String> SYNONYM_MAP = new HashMap<>(); static { SYNONYM_MAP.put("疼", "痛"); SYNONYM_MAP.put("胃疼", "胃痛"); SYNONYM_MAP.put("脑袋疼", "头痛"); // 继续扩充 } // 提取关键词后先做归一化再查字典 String normalized = SYNONYM_MAP.getOrDefault(keyword, keyword);4.3 模拟取号后用户状态丢失
现象:用户在预约页点完“模拟取号”,跳回主页发现流程状态还是显示“预约完成”,没有变成“排队中”。
原因:全局变量 current 在页面跳转过程中没有被正确更新——预约页更新了自己的局部状态,但没同步给 app.globalData.current,主页 onShow 读取的还是旧值。
解决:所有涉及流程状态变更的地方,统一走一个公共方法更新全局变量,不要各写各的。每次更新完 current,顺手调用 wx.setStorageSync 持久化一下,这样即使用户杀掉小程序重新打开,状态也不会丢。
4.4 MySQL 中文乱码
现象:科室名字在数据库里是乱码,页面上显示“?????”,或者反过来,页面显示正常但数据库存的是乱码。
原因:数据库表、连接 URL、Servlet 的 request/response 编码三层中至少有一层不是 UTF-8。最常见的是连接 URL 没有加 characterEncoding 参数。
解决:三处全部设置 UTF-8。
String url = "jdbc:mysql://服务器地址:3306/hospital?useUnicode=true&characterEncoding=UTF-8"; Connection conn = DriverManager.getConnection(url, "user", "password"); // Servlet 里设置编码 request.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8");创建表的时候也指定 utf8mb4,因为 utf8 在 MySQL 里存不了 emoji 表情。
4.5 Servlet 返回 JSON 但前端解析失败
现象:前端 wx.request 的 success 回调里收到数据,但 JSON.parse 报错,或者展示出来是 [object Object]。
原因:fastjson 序列化对象没问题,但如果你把 JSON 字符串直接写在 response.getWriter().write() 里,而字符串本身含有未转义的双引号,前端拿到就会解析失败。
解决:后端统一用 JSON.toJSONString 序列化,前端用 res.data 直接取对象,不要再 JSON.parse。如果 res.data 已经被小程序自动解析成了对象,再次 parse 就会报错——这个细节经常让人摸不着头脑。页面渲染时注意检查字段名,比如 fastjson 默认输出的字段是 Java 类的属性名,前端一定要对齐大小写。
5. 进阶优化空间与我对这套系统的理解
系统做了一个很完整的闭环:手动挂号覆盖普通用户,语音输入服务阅读障碍群体,图形选科服务不明确科室的群体,症状科普和看病笔记作为延伸功能提升粘性,模拟取号和模拟诊断把整个就医流程串成了可直接展示的 Demo。从课程设计和毕业设计的角度说,能把语音交互、NLP 推荐、小程序开发串起来,广度已经足够了。
但我看论文最后作者自己也提到了几个不足,我也同意:一是没有细分大科室,所有科室数据渲染用的是一个 for 循环,如果科室数量多,页面会非常长,需要做科室分组或搜索;二是智能推荐目前只是基于症状字典的简单打分,没有用到用户历史挂号数据做个性化;三是数据采集层面,没有预约量预测和热门科室的实时统计,而这些对医院排班和资源调度是有实际价值的。
如果想把系统做得更有说服力,我从工程角度看可以重点补两个方向。第一是科室推荐算法升级——把 HanLP 的推荐改成基于 TF-IDF 或 BM25 的相似度匹配,再叠加用户历史挂号偏好做加权排序。比如一个用户过去几个月在呼吸内科挂过两次号,现在说“咳嗽”,系统可以优先推呼吸内科而不是耳鼻喉科。
// 伪代码示意:基于历史记录加权 Map<String, Integer> scoreMap = diagnoseByKeyword(symptom); List<AppointmentHistory> historyList = queryHistory(userId); for (AppointmentHistory history : historyList) { String historySection = history.getSectionName(); scoreMap.put(historySection, scoreMap.getOrDefault(historySection, 0) + 2); } // 然后按 scoreMap 排序输出第二是把模拟取号升级成真实的叫号逻辑。现在模拟取号只是把预约记录转会排队表,但真实系统里要支持叫号信息的实时推送。微信小程序可以通过 WebSocket 或订阅消息实现,号到了给用户发一条模版消息。这样就从“模拟诊断完毕”的人工操作变成了真正的主治医生点击叫号后自动通知下一个患者。这部分代码不难写,但涉及多端同步,需要在数据库的表设计上多留一些状态位。
我在做类似系统时,最后养成了一个习惯——每次改完业务逻辑,都先把小程序端的流程状态图完整走一遍,从登录到选科、预约、取号、诊断,再回头清理一遍相关的数据库记录。因为这类系统的核心不是单个功能写得多漂亮,而是状态流和数据流是否一致,往往最容易出问题的就是跳转过程中某一步状态没同步,然后用户就卡在了一个尴尬的中间态。从那以后,我每次写完一块功能都强制自己完整跑一遍全链路流程,确认不会再出现状态丢失再继续往下做。希望这份系统的拆解能帮你在自己的项目里少走几步弯路。
本文还有配套的精品资源,点击获取