简介:本资源是一套基于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.class | com.lufeng.model | 患者实体,含idCardNo(身份证号)、triageStatus(分诊状态:0=待叫号,1=已叫号,2=已就诊)字段 | @Column(name="triage_status") |
UserController.class | com.lufeng.controller | 处理/triage/callNext请求,调用ScheduleJobService.callNext()触发叫号 | @ResponseBody返回JSON含nextNumber |
ScheduleJobService.class | com.lufeng.service | 叫号引擎核心:按科室ID查patient表,取triageStatus=0且deptId=?的最小queueNumber记录,更新为triageStatus=1 | @Transactional(isolation = Isolation.REPEATABLE_READ) |
UserRealm.class | com.lufeng.shiro | Shiro权限认证入口,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.sqllufeng_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
从那以后我每次加优先级逻辑,都强制走一遍「并发挂号→立即叫号→查数据库状态」的闭环验证,哪怕只是改个字段名。因为分诊系统的每一秒延迟,都可能让真实世界里的患者多等一次心跳。希望帮到你。
本文还有配套的精品资源,点击获取