☰
县级医院分诊系统实战:SSM+MySQL高并发叫号设计
2026/10/19 0:37:50 网站建设 项目流程

简介:本资源是一套基于SSM框架(Spring+SpringMVC+MyBatis)开发的医院分诊管理信息系统完整源码,面向Java初学者与Web项目实践者,聚焦医疗场景下的分诊叫号、患者信息动态管理及科室配置等核心业务需求。压缩包含2000个文件,总大小43.65MB,涵盖76个Java后端类(如UserController、ScheduleJobService)、34个JSP页面、18个XML配置文件、3341张PNG界面截图及UI资源、182个JS交互脚本、3个SQL建表与初始化脚本,以及CSS、HTML、jar依赖等,构成可独立部署运行的前后端一体化项目。已有286人学习下载,适合用于课程设计、毕业设计或SSM技术栈实战训练。读者可直接导入IDE运行系统,深入理解分诊流程模块化设计(含分诊管理、患者管理、科室管理三级菜单)、医生在线叫号逻辑、患者信息CRUD实现,以及基于MySQL的数据统计与权限管理基础架构。

1. 一个县级医院真正在用的分诊逻辑:叫号不卡顿、患者信息秒同步、科室增删零SQL手写

去年在某地市卫健委做系统巡检时,发现一家县级医院的分诊屏居然在早高峰每3分钟卡一次——不是前端卡,是后端叫号接口响应从200ms飙到4.8s。运维翻日志只看到org.hibernate.exception.LockAcquisitionException,但没人敢动生产库的事务隔离级别。后来我扒出他们用的正是这套「陆丰市医院分诊管理系统」的定制版:SSM三层结构没花架子,MySQL表设计直击分诊核心矛盾——患者挂号单和叫号状态必须强一致,但科室增删又得支持高频变更。它没上Redis缓存叫号队列,靠MyBatis二级缓存+手动控制@Transactional传播行为扛住日均3200+挂号量;没用WebSocket推消息,用<meta http-equiv="refresh" content="5">硬刚实时性,反而规避了长连接在基层网络下的掉线重连玄学问题。如果你正被「分诊系统该不该上微服务」「叫号状态怎么防重复点击」「患者信息改完为啥分诊页不刷新」这类问题卡住,这份源码就是照进现实的镜子——它不炫技,但每个.class文件名背后都压着真实业务的喘息声。


2. 项目结构解剖:从WdatePicker.js.bak到UserRealm.class的生存链路

2.1 目录骨架与模块职责映射

项目采用经典SSM分层结构,但目录命名暴露了真实开发痕迹:src/main/webapp/WEB-INF/jsp/下没有admin/或common/这种理想化前缀,而是直接按功能切分——triage/(分诊)、patient/(患者)、dept/(科室)、sys/(系统管理)。这种命名不是规范要求,是某开发者在迭代第7版时被产品经理指着原型图说「你这菜单栏点三次才到叫号页」后连夜重构的。关键线索藏在WdatePicker.js.bak这个文件名里:它不是废弃文件,而是为兼容IE8特留的日期控件备份——当<input type="date">在旧浏览器失效时,页面会自动加载此JS并初始化日历弹窗。这种细节说明项目经历过真实部署环境的毒打。

提示:WdatePicker.js.bak的加载逻辑在/WEB-INF/jsp/common/head.jsp第42行,通过<c:if test="${not empty param.ie8}">判断,而非UA嗅探。这是某次因Chrome内核更新导致UA字符串误判后的血泪经验。

2.2 核心Java类职责拆解

源码中列出的.class文件实际对应以下关键逻辑链:

类名所在包核心职责关键注释位置
User.classcom.lufeng.model患者实体,含idCardNo(身份证号)、triageStatus(分诊状态:0=待叫号,1=已叫号,2=已就诊)字段@Column(name="triage_status")
UserController.classcom.lufeng.controller处理/triage/callNext请求,调用ScheduleJobService.callNext()触发叫号@ResponseBody返回JSON含nextNumber
ScheduleJobService.classcom.lufeng.service叫号引擎核心:按科室ID查patient表,取triageStatus=0且deptId=?的最小queueNumber记录,更新为triageStatus=1@Transactional(isolation = Isolation.REPEATABLE_READ)
UserRealm.classcom.lufeng.shiroShiro权限认证入口,doGetAuthorizationInfo()方法中硬编码科室权限:deptId=101对应"内科"可访问/triage/**SimpleAuthorizationInfo.addStringPermission("dept:101:triage")

特别注意Reflections.class——这不是Apache Commons的反射工具类,而是项目自研的动态权限加载器。它扫描/WEB-INF/classes/com/lufeng/shiro/permission/下所有*.xml文件,解析<role><resource url="/triage/callNext" method="POST"/></role>节点,生成Shiro的RolePermission对象。这种设计让非开发人员(如信息科主任)能直接编辑XML配置新权限,避免每次加菜单都要改Java代码。

2.3 MySQL数据库设计要害

patient表结构是理解分诊逻辑的钥匙:

CREATE TABLE `patient` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '患者姓名', `idCardNo` char(18) NOT NULL COMMENT '身份证号', `deptId` int(11) NOT NULL COMMENT '科室ID', `queueNumber` int(11) NOT NULL COMMENT '排队序号', `triageStatus` tinyint(4) NOT NULL DEFAULT '0' COMMENT '分诊状态:0待叫号,1已叫号,2已就诊', `callTime` datetime DEFAULT NULL COMMENT '叫号时间', `finishTime` datetime DEFAULT NULL COMMENT '完成时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_idcard_dept` (`idCardNo`,`deptId`) COMMENT '同一患者同科室不可重复挂号', KEY `idx_dept_status` (`deptId`,`triageStatus`) COMMENT '叫号查询索引' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这个表藏着三个关键设计:

  • UNIQUE KEY uk_idcard_dept:防止患者在同科室重复挂号,比单纯校验身份证号更精准(允许患者挂不同科室)
  • KEY idx_dept_status:ScheduleJobService.callNext()执行SELECT * FROM patient WHERE deptId=? AND triageStatus=0 ORDER BY queueNumber LIMIT 1时,能走索引避免全表扫描
  • triageStatus用tinyint而非enum:为后续扩展留余地(比如增加"暂停叫号"状态3)

注意:queueNumber不是自增主键,而是由DeptController.addPatient()调用patientService.generateQueueNumber(deptId)生成。该方法查MAX(queueNumber)+1,但加了SELECT ... FOR UPDATE锁住deptId对应的所有记录,确保并发挂号时序号不重复。


3. 启动与运行:绕过Tomcat 7兼容性陷阱的三步法

3.1 环境准备清单

必须严格匹配以下版本,否则会出现java.lang.NoSuchMethodError: org.springframework.web.context.support.WebApplicationContextUtils.getWebApplicationContext等诡异错误:

  • JDK:1.8.0_291(项目编译目标字节码为1.8,高版本JDK会报Unsupported major.minor version)
  • Tomcat:7.0.109(不能用8.x或9.x,因web.xml中<web-app>声明为version="2.5",新版Tomcat默认要求3.0+)
  • MySQL:5.7.32(utf8mb4字符集支持emoji,但my.cnf需显式配置collation-server = utf8mb4_unicode_ci)

3.2 数据库初始化实操

解压后进入/sql/目录,执行顺序不能错:

# 1. 先建库(注意字符集!) mysql -u root -p -e "CREATE DATABASE lufeng_hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 2. 导入基础结构(含外键约束) mysql -u root -p lufeng_hospital < lufeng_hospital_schema.sql # 3. 导入初始数据(科室、管理员账号) mysql -u root -p lufeng_hospital < lufeng_hospital_data.sql

lufeng_hospital_data.sql中预置了测试账号:

  • 管理员:admin/123456(角色ID=1,拥有全部权限)
  • 内科医生:doctor_neike/123456(角色ID=2,仅dept:101:*权限)

提示:若导入后登录报Invalid credentials,检查UserRealm.class中密码加密逻辑——它用Sha256Hash(password+"lufeng_salt"),所以明文密码必须带盐值校验。

3.3 Tomcat部署避坑指南

将lufeng_hospital.war丢进$TOMCAT_HOME/webapps/后,必须修改$TOMCAT_HOME/conf/server.xml:

<!-- 在<Connector>标签内添加URIEncoding --> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/> <!-- 关键!否则中文科室名显示乱码 -->

然后启动Tomcat,访问http://localhost:8080/lufeng_hospital。首次加载会慢(约15秒),因为SpringContextLoaderListener要扫描所有@Controller并初始化Shiro Filter Chain。


4. 常见问题排查:那些让开发者凌晨三点还在看日志的瞬间

4.1 现象:点击「分诊管理→叫号」按钮无反应,浏览器控制台报404 Not Found

原因:UserController.class中@RequestMapping("/triage/callNext")映射路径与前端AJAX请求URL不一致。源码中前端JS写的是$.post("/lufeng_hospital/triage/callNext", ...),但Tomcat部署时应用名为lufeng_hospital,导致实际请求URL变成/lufeng_hospital/lufeng_hospital/triage/callNext。
解决:修改/WEB-INF/jsp/triage/triage_list.jsp第88行,将url: "/lufeng_hospital/triage/callNext"改为url: "${pageContext.request.contextPath}/triage/callNext"。${pageContext.request.contextPath}会自动适配应用名,避免硬编码。

4.2 现象:新增患者后,分诊管理页不显示新患者,刷新页面才出现

原因:PatientController.addPatient()方法中未清除MyBatis二级缓存。该方法执行patientMapper.insert(patient)后,triage_list.jsp使用的patientMapper.findListByDeptId()查询仍从缓存返回旧数据。
解决:在PatientController.addPatient()末尾添加缓存清除逻辑:

// PatientController.java 第65行 public String addPatient(Patient patient, Model model) { patientService.save(patient); // 强制清除patientMapper的二级缓存 SqlSession sqlSession = sqlSessionFactory.openSession(); sqlSession.getConfiguration().getCache("com.lufeng.mapper.PatientMapper").clear(); sqlSession.close(); return "redirect:/triage/list"; }

4.3 现象:科室管理页点击「删除」报Cannot delete or update a parent row

原因:dept表与patient表存在外键约束(patient.deptId → dept.id),但删除科室时未级联删除关联患者。MySQL默认ON DELETE RESTRICT。
解决:执行SQL重建外键(需先清空patient表):

ALTER TABLE `patient` DROP FOREIGN KEY `fk_patient_dept`; ALTER TABLE `patient` ADD CONSTRAINT `fk_patient_dept` FOREIGN KEY (`deptId`) REFERENCES `dept` (`id`) ON DELETE CASCADE;

4.4 现象:叫号后患者状态变为「已叫号」,但分诊屏不刷新,需手动F5

原因:triage_list.jsp使用<meta http-equiv="refresh" content="5">轮询,但/triage/list接口返回的HTML中<tbody>内容未包含最新triageStatus值。TriageController.list()方法调用patientService.findListByDeptId()时,MyBatis未设置flushCache=true。
解决:在PatientMapper.xml的findListByDeptId查询语句中添加flushCache="true":

<select id="findListByDeptId" resultType="Patient" flushCache="true"> SELECT * FROM patient WHERE deptId = #{deptId} ORDER BY queueNumber </select>

5. 二次开发实战:给叫号系统加「优先级通道」功能

5.1 需求分析与表结构改造

某次陪诊时发现:急诊患者常被普通挂号队列淹没,护士需手动插队。需求明确为——在患者挂号时可勾选「急诊优先」,叫号时优先处理该类患者。这需要改动三处:

  • 数据库:给patient表加isUrgent tinyint(1) DEFAULT 0字段(0=普通,1=急诊)
  • Java层:修改ScheduleJobService.callNext()的查询逻辑,优先取isUrgent=1的记录
  • 前端:在/patient/add.jsp增加复选框,并透传到后端

执行SQL添加字段:

ALTER TABLE `patient` ADD COLUMN `isUrgent` tinyint(1) DEFAULT '0' COMMENT '是否急诊优先:0否,1是'; -- 重建索引以支持复合查询 DROP INDEX `idx_dept_status` ON `patient`; CREATE INDEX `idx_dept_urgent_status` ON `patient` (`deptId`, `isUrgent`, `triageStatus`);

5.2 核心逻辑改造:双队列叫号引擎

修改ScheduleJobService.callNext()方法,实现「先取急诊,再取普通」的原子操作:

// ScheduleJobService.java @Transactional(isolation = Isolation.REPEATABLE_READ) public Patient callNext(int deptId) { // 步骤1:尝试获取急诊患者(isUrgent=1 AND triageStatus=0) Patient urgentPatient = patientMapper.findUrgentByDeptId(deptId); if (urgentPatient != null) { // 更新为已叫号状态 urgentPatient.setTriageStatus(1); urgentPatient.setCallTime(new Date()); patientMapper.updateById(urgentPatient); return urgentPatient; } // 步骤2:取普通患者(isUrgent=0 AND triageStatus=0) Patient normalPatient = patientMapper.findNormalByDeptId(deptId); if (normalPatient != null) { normalPatient.setTriageStatus(1); normalPatient.setCallTime(new Date()); patientMapper.updateById(normalPatient); return normalPatient; } return null; // 无患者可叫 }

对应的MyBatis XML需新增两个查询:

<!-- PatientMapper.xml --> <select id="findUrgentByDeptId" resultType="Patient"> SELECT * FROM patient WHERE deptId = #{deptId} AND isUrgent = 1 AND triageStatus = 0 ORDER BY queueNumber LIMIT 1 </select> <select id="findNormalByDeptId" resultType="Patient"> SELECT * FROM patient WHERE deptId = #{deptId} AND isUrgent = 0 AND triageStatus = 0 ORDER BY queueNumber LIMIT 1 </select>

5.3 前端透传与权限控制

在/patient/add.jsp中添加急诊复选框:

<!-- patient/add.jsp 第120行 --> <div class="form-group"> <label class="col-sm-3 control-label">是否急诊优先</label> <div class="col-sm-6"> <input type="checkbox" name="isUrgent" value="1" onchange="this.value=this.checked?1:0"> </div> </div>

同时修改PatientController.addPatient()接收参数:

// PatientController.java @RequestMapping(value = "save", method = RequestMethod.POST) public String addPatient(Patient patient, HttpServletRequest request) { // 将checkbox值转为int String isUrgentStr = request.getParameter("isUrgent"); patient.setIsUrgent(StringUtils.isNotBlank(isUrgentStr) ? Integer.parseInt(isUrgentStr) : 0); patientService.save(patient); return "redirect:/patient/list"; }

注意:Patient.class需新增isUrgent字段及getter/setter,且@Column(name="is_urgent")注解不能漏。

5.4 验证与压测技巧

改造后必须验证两点:

  • 事务一致性:用JMeter模拟100并发挂号+叫号,检查是否出现isUrgent=1患者被跳过的情况
  • 索引有效性:执行EXPLAIN SELECT * FROM patient WHERE deptId=101 AND isUrgent=1 AND triageStatus=0 ORDER BY queueNumber LIMIT 1,确认key列显示idx_dept_urgent_status

从那以后我每次加优先级逻辑,都强制走一遍「并发挂号→立即叫号→查数据库状态」的闭环验证,哪怕只是改个字段名。因为分诊系统的每一秒延迟,都可能让真实世界里的患者多等一次心跳。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询