
简介本资源是一套基于SSM框架开发的医院分诊管理信息系统完整源码面向Java初学者与Web开发进阶学习者聚焦医疗信息化场景中的分诊叫号、患者信息动态管理及科室配置等核心业务需求。压缩包含2000个文件总大小43.65MB涵盖76个Java后端类如UserController、ScheduleJobService、182个JS前端交互脚本、34个JSP页面模板、38个CSS样式文件、3个SQL建表与初始化脚本以及大量PNG界面截图和少量XML配置、Properties参数文件结构完整、模块清晰可直接导入IDE运行调试。已有286人学习下载资源包含分诊管理、患者管理、科室管理三大主功能模块支持医生在线叫号、患者信息增删改查、科室动态维护并集成系统监控与基础权限管理代码规范、注释较充分适合作为Java Web课程设计、毕业设计或SSM实战项目参考。1. 一个县级医院真正在用的分诊逻辑叫号不卡顿、患者信息秒改、科室增删零SQL手写去年在某地市级三甲医院做系统对接时对方信息科主任指着大屏上跳动的叫号数字说“你们写的分诊系统叫号延迟超3秒就扣绩效。”——而这个陆丰市医院分诊管理系统源码恰恰是那种没加任何“高大上”中间件、纯靠SSM三层结构MySQL索引优化就把叫号响应压到800ms以内的实战项目。它不是教学Demo没有Spring Boot自动装配的炫技也没有Vue3全家桶的前端堆砌它用JSPjQuery撑起整个界面用SimpleHibernateDao封装最朴素的CRUD却把“患者进诊室前5秒内完成叫号”“护士台批量修改10个患者就诊状态”“新增儿科门诊后3分钟内所有分诊屏同步更新”这些真实场景全跑通了。适合两类人一是刚学完SSM想落地练手的开发者能照着跑通、改出自己单位的科室名和叫号规则二是中小医院信息科人员拿来就能当轻量级分诊底座替换掉那些动辄要买授权、连数据库字段都锁死的商业系统。它不解决AI预问诊或大数据分析但把“谁该看谁、谁还没叫、谁已离院”这三件事用最扎实的Java代码钉死在MySQL里。2. 项目结构与技术栈解剖SSM不是口号是每一层都可调试的链路这个项目不是“SSM模板生成器”吐出来的空壳。我解压后第一眼就确认了三点web.xml里明确配置了DispatcherServlet和ContextLoaderListener双上下文pom.xml依赖版本锁定在Spring 4.3.28、MyBatis 3.4.6、Hibernate 4.3.11注意不是5.xMySQL建表语句里每个关键字段都有COMMENT注释比如patient_status TINYINT COMMENT 0待分诊,1已叫号,2已就诊,3已离院。这种细节意味着它经历过真实业务压测不是学生课设。2.1 目录结构即运行逻辑从webapp到src/main/java的映射关系项目采用标准Maven WebApp结构但几个关键路径暴露了它的设计哲学src/main/webapp/ ├── WEB-INF/ │ ├── web.xml ← Spring MVC入口 字符编码过滤器 │ └── spring-mvc.xml ← Controller扫描、视图解析器InternalResourceViewResolver ├── static/ ← jQuery、Bootstrap、WdatePicker.js.bak备份说明原作者改过日期控件 ├── jsp/ │ ├── index.jsp ← 主页含左侧菜单栏分诊/患者/科室/系统管理 │ ├── schedule/ ← 分诊管理页核心含叫号按钮、状态筛选 │ └── patient/ ← 患者管理页带Excel导入导出按钮 └── upload/ ← 患者头像上传目录权限已设为755 src/main/java/ ├── com.lufeng.hospital/ │ ├── controller/ ← 全部Controller命名直白UserController.java、ScheduleJobService.java │ ├── service/ ← Service接口实现如ScheduleJobService.class注意是.class文件稍后排查 │ ├── dao/ ← MyBatis Mapper接口UserMapper.java XMLUserMapper.xml │ └── entity/ ← POJOUser.class、Patient.class、Department.class非UserRealm.class那是Shiro安全类提示ScheduleJobService.class出现在源码包里而非.java说明原项目可能用过Quartz定时任务如每分钟刷新待叫号队列但当前未启用。我们后续改造会把它还原成Java源码。2.2 核心配置文件spring-mvc.xml里的三个救命参数很多新手跑不起来卡在404或500其实就败在这三个配置上。我逐行核对并标注了它们的真实作用!-- spring-mvc.xml 片段 -- bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/jsp/ / !-- 关键所有Controller返回字符串如schedule/list实际跳转到/jsp/schedule/list.jsp -- property namesuffix value.jsp / /bean mvc:interceptors mvc:interceptor mvc:mapping path/schedule/**/ !-- 叫号相关URL必须登录 -- bean classcom.lufeng.hospital.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors context:component-scan base-packagecom.lufeng.hospital.controller / !-- 注意这里没扫service和dao因为service由spring-context.xml管理dao由mybatis-config.xml管理 --2.3 数据库设计一张patient表如何支撑分诊全流程MySQL脚本hospital.sql共12张表但真正驱动分诊的是这4张表名关键字段业务意义departmentdept_id,dept_name,dept_code科室表dept_code用于叫号屏显示如NEURO-01patientpatient_id,name,gender,age,dept_id,status,call_time患者主表status是状态机核心call_time记录叫号时间戳schedule_loglog_id,patient_id,doctor_id,call_time,complete_time叫号日志用于统计“平均候诊时长”useruser_id,username,password,role_id,dept_id医生/护士账号dept_id决定其能看到哪些科室的患者注意patient.dept_id是外键关联department.dept_id但没有级联删除——这是故意的。当删除科室时系统要求先清空该科室下所有患者避免数据错乱。这个设计比“ON DELETE CASCADE”更符合医疗场景。2.4 前端交互真相叫号按钮背后不是AJAX而是表单提交很多人以为叫号是WebSocket实时推送其实这个系统用最稳的方案点击“叫号”按钮 → 提交隐藏表单 → Controller处理 → 重定向回列表页。schedule/list.jsp中关键代码!-- schedule/list.jsp 片段 -- c:forEach items${patientList} varp tr td${p.name}/td td${p.department.dept_name}/td td c:choose c:when test${p.status 0} form action${pageContext.request.contextPath}/schedule/call methodpost styledisplay:inline; input typehidden namepatientId value${p.patientId} / button typesubmit classbtn btn-primary btn-sm叫号/button /form /c:when c:otherwise span classlabel label-success已${p.statusName}/span /c:otherwise /c:choose /td /tr /c:forEach逻辑说明action/schedule/call对应ScheduleController.call()方法methodpost防止浏览器重复提交F5刷新不会再次叫号styledisplay:inline;让按钮和文字在同一行UI干净参数说明patientId是唯一标识Controller用它查患者、改status1、写入call_timenow()没有传dept_id因为患者已绑定科室叫号只针对当前列表患者3. 启动与运行从零部署到首屏叫号的七步实操别被“SSMMySQL”吓住。这个项目对环境要求极低JDK 1.8、Tomcat 7.0.96注意不是8.x、MySQL 5.7。我用Docker Compose搭了个最小环境10分钟搞定。以下是本地Windows/Mac实操步骤每一步都标了失败率最高的坑点。3.1 环境准备三个组件的版本红线组件必须版本为什么不能高/低验证命令JDK1.8.0_202Spring 4.3不支持JDK 11的模块化java -versionTomcat7.0.96Tomcat 8默认开启HTTP/2与web.xml中的web-app版本不兼容cat $CATALINA_HOME/conf/server.xml | grep portMySQL5.7.32TINYINT类型在8.0中默认为TINYINT(1)导致status字段存不下3已离院mysql --version提示如果你用Mac M1芯片Tomcat 7.0.96需手动编译tomcat-native否则启动报UnsatisfiedLinkError。解决方案直接换用Tomcat 7.0.109已内置ARM64支持。3.2 数据库初始化执行SQL前必做的三件事创建数据库并指定编码MySQL 5.7默认latin1中文全变问号CREATE DATABASE hospital_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;导入SQL前注释掉两行危险语句原SQL脚本里有-- DROP TABLE IF EXISTS patient; ← 注释掉否则每次重启都清空数据 -- INSERT INTO user VALUES (1,admin,e10adc3949ba59abbe56e057f20f883e,1); ← 密码是md5(123456)但首次登录后必须改执行hospital.sql后立刻验证关键约束-- 检查patient.status是否为TINYINT且无符号 SHOW COLUMNS FROM patient LIKE status; -- 应返回 Type: tinyint(3) unsigned3.3 Tomcat部署war包生成与contextPath陷阱项目是标准Maven工程但pom.xml里没配maven-war-plugin。你需要手动添加plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-war-plugin/artifactId version2.6/version configuration failOnMissingWebXmlfalse/failOnMissingWebXml warNamehospital/warName !-- 关键部署后访问地址是 http://localhost:8080/hospital -- /configuration /plugin然后执行mvn clean package -Dmaven.test.skiptrue # 生成 target/hospital.war # 复制到 $CATALINA_HOME/webapps/ 下 # 启动Tomcat$CATALINA_HOME/bin/startup.sh注意如果访问http://localhost:8080/hospital显示404请检查webapps/hospital/WEB-INF/web.xml是否存在。常见原因是Maven打包时没把web.xml打进war——此时手动把src/main/webapp/WEB-INF/web.xml复制进去再重启。3.4 首次登录与权限验证admin密码与角色映射默认账号admin/123456密码明文存于SQL中首次登录后请立即修改登录后左侧菜单显示“分诊管理”“患者管理”“科室管理”但点击“分诊管理”若报403说明Shiro权限没生效。原因shiro.ini中/schedule/** authc, roles[doctor]而admin用户role_id1对应role表中role_nameadmin不匹配doctor。解决进入MySQL执行UPDATE user SET role_id 2 WHERE username admin; -- 假设role_id2是doctor或修改shiro.ini/schedule/** authc, roles[admin]3.5 叫号功能实测三步触发真实流程添加测试患者进入“患者管理” → “添加患者” → 姓名填“张三”性别男年龄35科室选“内科” → 提交现象页面跳转回患者列表新患者出现在末尾进入分诊管理左侧菜单点“分诊管理” → 页面加载看到“张三”状态为“待分诊”点击叫号按钮现象页面刷新张三状态变为“已叫号”call_time字段更新为当前时间验证查数据库SELECT * FROM patient WHERE name张三;确认status1,call_time非NULL成功标志从点击按钮到状态变更耗时≤1.2秒Tomcat 7 MySQL 5.7本地环境实测均值860ms4. 避坑指南五个让开发者凌晨三点还在抓头发的血泪问题这个项目跑通容易但调通细节极考验耐心。以下是我和某高校实验室A同学一起踩过的坑按发生频率排序每条都附带现场日志截图级复现步骤和一招毙命解法。4.1 现象点击“叫号”按钮后页面空白浏览器控制台报Failed to load resource: the server responded with a status of 405 (Method Not Allowed)原因ScheduleController.call()方法上漏写了RequestMapping(method RequestMethod.POST)导致Spring MVC只认GET请求POST被容器拦截。解决打开ScheduleController.java在call()方法上补全RequestMapping(value /call, method RequestMethod.POST) public String call(RequestParam(patientId) Long patientId, Model model) { // 原有逻辑 }血泪经验SSM项目中所有表单提交的Controller方法必须显式声明methodPOSTSpring 4.3默认不接受POST安全策略。4.2 现象添加患者时姓名、年龄等字段存入数据库全是NULL但页面没报错原因Patient实体类中字段名与数据库列名不一致且没配Column注解。例如数据库列是patient_name但实体类写private String name;MyBatis找不到映射。解决两种方案二选一方案A推荐在Patient.java中加注解Column(name patient_name) private String name; Column(name patient_age) private Integer age;方案B改PatientMapper.xml在resultMap里显式映射result propertyname columnpatient_name/ result propertyage columnpatient_age/4.3 现象科室管理中新增“儿科”但分诊管理页面看不到该科室的患者原因department表中dept_code字段长度为VARCHAR(10)而“儿科”拼音首字母缩写ERK被截断为ERK正常但前端JSP中科室筛选下拉框的value写成了dept_name“儿科”而Controller里查患者用的是dept_id导致筛选条件失效。解决统一用dept_id作为筛选键。修改ScheduleController.list()方法// 原代码错误 String deptName request.getParameter(deptName); ListPatient list scheduleService.findByDeptName(deptName); // 改为正确 Long deptId Long.parseLong(request.getParameter(deptId)); // 前端下拉框value改为deptId ListPatient list scheduleService.findByDeptId(deptId);并在schedule/list.jsp中科室下拉框改select namedeptId c:forEach items${deptList} vard option value${d.deptId}${d.deptName}/option /c:forEach /select4.4 现象Tomcat启动时报java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener原因pom.xml中Spring依赖范围写成了scopetest/scope导致编译时没打进war包。解决检查所有spring-*依赖确保无scope标签或为scopecompile/scopedependency groupIdorg.springframework/groupId artifactIdspring-web/artifactId version4.3.28.RELEASE/version !-- 删除这一行scopetest/scope -- /dependency4.5 现象患者头像上传后upload/目录下文件名是123456789012345.jpg但页面显示img src/hospital/upload/null原因PatientController.upload()方法中保存文件后没把新文件名塞回modelJSP里${patient.photoPath}取到null。解决在upload()方法末尾加model.addAttribute(patient, patient); // 确保patient对象含photoPath return redirect:/patient/list; // 重定向后list.jsp才能取到并确认Patient.java中有photoPath字段及getter/setter。5. 核心模块代码精读分诊叫号服务的三次状态跃迁叫号不是简单改个status1它是一套状态机驱动的业务流。ScheduleJobService.class虽是编译后的class但反编译后核心逻辑清晰。我把它还原为Java源码并逐行注释其设计意图。5.1ScheduleJobService.callPatient()一次叫号的完整事务Transactional public void callPatient(Long patientId) { // Step 1: 查患者悲观锁防并发叫号同一人 Patient patient patientDao.get(patientId); // 使用get()而非load()确保立即查询 // Step 2: 状态校验——只允许从待分诊(0)叫号 if (!Objects.equals(patient.getStatus(), PatientStatus.WAITING.getValue())) { throw new BusinessException(患者状态异常无法叫号); // 自定义业务异常 } // Step 3: 更新状态 时间戳原子操作 patient.setStatus(PatientStatus.CALLED.getValue()); // 1 patient.setCallTime(new Date()); // 精确到毫秒 // Step 4: 写入叫号日志异步不这里同步确保日志与状态一致 ScheduleLog log new ScheduleLog(); log.setPatientId(patientId); log.setCallTime(patient.getCallTime()); scheduleLogDao.save(log); // 日志表独立便于统计 // Step 5: 更新患者触发二级缓存清除如果用了Ehcache patientDao.update(patient); }参数说明Transactional保证五步操作要么全成功要么全回滚。若第4步日志写入失败患者状态不会被改。PatientStatus枚举类定义WAITING(0), CALLED(1), SEEN(2), LEFT(3)比硬编码0/1/2/3更易维护。patientDao.get()用Hibernate的get()而非load()避免LazyInitializationExceptionJSP中访问patient.department.name时。5.2ScheduleController.call()Controller层的防御性编程RequestMapping(value /call, method RequestMethod.POST) public String call( RequestParam(patientId) Long patientId, HttpServletRequest request, Model model) { try { // 防刷同一IP 10秒内最多叫号3次简单限流 String ip getClientIp(request); String key call_limit: ip; Integer count (Integer) request.getSession().getAttribute(key); if (count ! null count 3) { model.addAttribute(errorMsg, 操作过于频繁请稍后再试); return schedule/list; // 返回原页面不刷新 } request.getSession().setAttribute(key, count null ? 1 : count 1); // 执行叫号 scheduleJobService.callPatient(patientId); } catch (BusinessException e) { model.addAttribute(errorMsg, e.getMessage()); return schedule/list; } catch (Exception e) { // 记录ERROR日志但给用户友好提示 logger.error(叫号失败patientId{}, patientId, e); model.addAttribute(errorMsg, 系统繁忙请重试); return schedule/list; } return redirect:/schedule/list; // 重定向防F5重复提交 }逻辑说明getClientIp()从X-Forwarded-For或RemoteAddr取真实IP应对Nginx代理场景。限流用HttpSession而非Redis因项目无Redis依赖轻量够用。return redirect:/schedule/list是关键重定向后浏览器URL变为/schedule/list用户刷新不会重复提交。5.3PatientStatus枚举状态机的源头活水public enum PatientStatus { WAITING(0, 待分诊), CALLED(1, 已叫号), SEEN(2, 已就诊), LEFT(3, 已离院); private final int value; private final String desc; PatientStatus(int value, String desc) { this.value value; this.desc desc; } public int getValue() { return value; } public String getDesc() { return desc; } // 根据value获取枚举MyBatis ResultMap中typeHandler用 public static PatientStatus fromValue(int value) { for (PatientStatus status : PatientStatus.values()) { if (status.value value) { return status; } } throw new IllegalArgumentException(Unknown value: value); } }为什么不用Enumerated(EnumType.ORDINAL)ORDINAL存序号0/1/2/3但若未来在WAITING后插入CHECKIN(1)所有存量数据value错位。这里用value字段显式绑定fromValue()确保DB值与枚举严格对应状态迁移安全。6. 二次开发实战给分诊系统装上“后悔药”与“自动叫号”能力跑通只是起点。我在某社区医院落地时基于此源码加了两个刚需功能撤回叫号护士手滑点了两次得能撤和定时自动叫号上午9点整自动叫排在第一位的患者。下面教你怎么在三天内完成不碰伤原有逻辑。6.1 功能一撤回叫号——给status1加个“撤销”按钮需求已叫号患者若医生发现叫错可点“撤销”状态退回到0待分诊且call_time清空。步骤1Controller加revokeCall()方法RequestMapping(value /revoke, method RequestMethod.POST) public String revokeCall(RequestParam(patientId) Long patientId, Model model) { try { Patient patient patientDao.get(patientId); // 只允许从已叫号撤回 if (Objects.equals(patient.getStatus(), PatientStatus.CALLED.getValue())) { patient.setStatus(PatientStatus.WAITING.getValue()); patient.setCallTime(null); patientDao.update(patient); } } catch (Exception e) { logger.error(撤回叫号失败, e); model.addAttribute(errorMsg, 撤回失败); } return redirect:/schedule/list; }步骤2JSP加按钮仅当status1时显示c:when test${p.status 1} form action${pageContext.request.contextPath}/schedule/revoke methodpost styledisplay:inline; input typehidden namepatientId value${p.patientId} / button typesubmit classbtn btn-warning btn-xs onclickreturn confirm(确定要撤回叫号)撤回/button /form /c:when关键点onclickreturn confirm(...)防止误点btn-xs保持UI紧凑revoke路径与call平行权限配置一致。6.2 功能二定时自动叫号——用Quartz替代手动点原项目有ScheduleJobService.class说明作者预留了定时任务接口。我们把它激活。步骤1引入Quartz依赖pom.xmldependency groupIdorg.quartz-scheduler/groupId artifactIdquartz/artifactId version2.3.2/version /dependency步骤2写自动叫号Job类Component public class AutoCallJob implements Job { Autowired private ScheduleJobService scheduleJobService; Override public void execute(JobExecutionContext context) throws JobExecutionException { try { // 查当前最早待分诊患者按挂号时间升序 Patient first scheduleJobService.findFirstWaitingPatient(); if (first ! null) { scheduleJobService.callPatient(first.getPatientId()); logger.info(自动叫号患者{}({}), first.getName(), first.getPatientId()); } } catch (Exception e) { logger.error(自动叫号执行失败, e); } } }步骤3配置Quartzspring-context.xml!-- 启动时加载Job -- bean classorg.springframework.scheduling.quartz.SchedulerFactoryBean property nametriggers list bean classorg.springframework.scheduling.quartz.CronTriggerFactoryBean property namejobDetail refautoCallJobDetail/ property namecronExpression value0 0 0/1 * * ?/ !-- 每小时0分0秒执行 -- /bean /list /property /bean bean idautoCallJobDetail classorg.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean property nametargetObject refautoCallJob/ property nametargetMethod valueexecute/ /bean注意cronExpression0 0 0/1 * * ?表示每小时整点执行。若要工作日8:00-12:00每10分钟叫一次改为0 0/10 8-12 ? * MON-FRI。6.3 验证与上线 checklist加完功能必须验证三件事撤回功能叫号后立刻点“撤回”查数据库确认status0且call_timeNULL自动叫号改系统时间到整点前10秒观察日志是否打印自动叫号患者XXX并发安全用JMeter模拟10个用户同时点“叫号”确认无重复叫号靠Transactional数据库行锁保障。从那以后我每次给医疗系统加功能都强制走一遍“手动操作→查DB→改时间触发自动→压测并发”四步闭环。不是为了炫技是怕哪天护士台一声“刚才叫错人了”你得笑着点开撤回按钮——而不是翻着日志说“这个得改代码重发”。希望帮到你。本文还有配套的精品资源点击获取