资讯详情

SSM分诊系统:基于状态机的医疗资源调度实战

📅 2026/10/18 17:45:41 | 华诺云谱 👁 阅读
SSM分诊系统:基于状态机的医疗资源调度实战
简介本资源是一套基于SSM框架SpringSpringMVCMyBatis开发的医院分诊管理信息系统完整源码面向Java初学者与Web项目实践者聚焦医疗场景下的分诊叫号、患者信息动态管理及科室配置等核心业务需求。压缩包含2000个文件总大小43.65MB涵盖76个Java后端类如UserController、ScheduleJobService、34个JSP页面、18个XML配置文件、3341张PNG界面截图及UI资源、182个JS交互脚本、3个SQL建表与初始化脚本以及CSS、HTML、jar依赖等完整工程要素结构清晰、模块解耦明确。已有286人学习下载可直接导入IDE运行支持医生在线叫号、患者信息增删改查、科室动态维护并集成系统监控与基础权限管理功能附带HibernateDao、JsonMapper等典型工具类实现是理解SSM整合、医疗信息化业务建模与前后端协同开发的优质实战案例。1. 分诊不是排号是医疗资源调度的实时决策一套能跑在真实医院测试环境里的 SSM 分诊系统为什么必须带状态机和科室权重你见过凌晨三点还在手动改 Excel 叫号表的分诊护士吗我带过的某三甲附属医院模拟项目X里分诊台用的还是纸质登记白板手写叫号——不是不想上系统是试过三套“能跑通”的毕设级代码一到高峰期就卡死、漏号、重复叫、状态不同步。这套陆丰市医院分诊管理系统源码SSMMySQL最硬的点不是它用了 SpringSpringMVCMyBatis而是它把「分诊」这件事拆解成了可落地的状态流患者挂号 → 科室初筛 → 分诊队列入队 → 医生确认接收 → 就诊中 → 完成/转科/退号。每个环节都带数据库状态标记status字段值为 0-5前端按钮显隐、后端接口权限、统计报表维度全由这个状态驱动。它不追求炫酷大屏但能扛住门诊高峰每分钟 20 新增患者、30 叫号操作、5 科室并行调度的真实压力。适合正在做 Java 课设/毕设、需要交一份「有业务逻辑、有数据闭环、能现场演示不翻车」的同学也适合某公司内部快速搭一个轻量分诊原型拿去和医务科对需求。别被标题里的“陆丰市”误导——地名只是示例核心是这套分诊状态机设计、SSM 分层解耦写法、以及 MySQL 表结构如何支撑动态科室扩展。2. 总体架构设计三层结构与四个核心模块的职责划分这套系统严格遵循 SSM 经典分层Controller 层只做参数校验和跳转Service 层封装完整业务逻辑含事务控制DAO 层专注单表 CRUD。所有模块均通过 Spring 的Service和Autowired注入无硬编码依赖。整个系统围绕「分诊流」展开划分为四个高内聚模块分诊管理核心调度、患者管理主数据、科室管理配置中心、系统管理权限与监控。模块间通过统一的数据模型如Patient、Department实体类和状态码通信避免跨模块直接调用 DAO。特别注意它没有用 Shiro 或 Spring Security 做复杂 RBAC而是用UserRealm类结合RoleController实现角色-菜单-按钮三级权限控制既满足医院行政、医生、护士不同角色的操作隔离又大幅降低学习成本——A同学用三天就搞懂了权限怎么配。2.1 分诊管理模块状态机驱动的实时叫号引擎分诊管理是系统心脏其核心是ScheduleJobService类实现的定时轮询 手动触发双机制。它不依赖 Quartz 集群而是用 MySQL 的FOR UPDATE行锁保障并发安全-- 分诊叫号时锁定当前待叫号患者按科室优先级排序 SELECT * FROM patient_queue WHERE dept_id ? AND status 0 ORDER BY priority DESC, create_time ASC LIMIT 1 FOR UPDATE;提示patient_queue表不是简单复制patient表而是分诊专用队列表含queue_no叫号序号、call_time叫号时间、doctor_id接诊医生等字段。每次叫号先更新patient_queue.status1已叫号再异步通知前端 WebSocket 推送源码中WdatePicker.js.bak是日期控件备份实际用的是jquery.datetimepicker.js但叫号逻辑在UserController.callNext()方法里。UserController.class中的callNext方法是关键入口// UserController.java RequestMapping(value /callNext, method RequestMethod.POST) ResponseBody public Result callNext(RequestParam(deptId) Long deptId) { try { // 1. 检查该科室是否有待叫号患者 PatientQueue queue scheduleJobService.getWaitingPatient(deptId); if (queue null) { return Result.fail(当前科室无待叫号患者); } // 2. 更新状态0→1已叫号记录叫号时间 queue.setStatus(1); queue.setCallTime(new Date()); scheduleJobService.updateQueueStatus(queue); // 3. 返回前端所需信息含语音播报文本 return Result.success(MapUtil.builder() .put(queueNo, queue.getQueueNo()) .put(name, queue.getPatientName()) .put(deptName, queue.getDeptName()) .build()); } catch (Exception e) { log.error(叫号失败科室ID{}, deptId, e); return Result.fail(叫号异常请重试); } }这段代码背后是典型的「查-改-返」三步原子操作。getWaitingPatient方法内部用SELECT ... FOR UPDATE锁定行确保高并发下不会重复叫同一人updateQueueStatus走 MyBatis 的updateByPrimaryKeySelective只更新必要字段。参数deptId来自前端点击科室卡片时传入意味着医生可自由切换查看不同科室队列——这比固定页面更贴合真实分诊台多科室并行场景。2.2 患者管理模块主数据治理与动态字段扩展患者信息不是静态录入而是支持「就诊过程动态补全」。Patient.class实体类包含基础字段id,name,id_card,phone和就诊相关字段visit_date,symptom,diagnosis。关键设计在于添加患者时PatientController.add()只强制填写name和id_card后续在分诊或就诊环节医生可通过PatientController.updateForVisit()补充症状、初步诊断等临床信息。这种设计避免了挂号时信息收集不全导致流程阻塞。数据库表patient结构精简但patient_ext扩展表预留了ext_json字段TEXT 类型用于存储非结构化补充信息如过敏史 JSON 字符串。源码中虽未直接使用但JsonMapper.class已集成 Jackson随时可启用// JsonMapper.java 封装了常用序列化方法 public class JsonMapper extends ObjectMapper { private static final long serialVersionUID 1L; private static JsonMapper mapper; static { mapper new JsonMapper(); // 全局配置空字符串转 null忽略未知字段 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); } public static T T fromJson(String jsonString, ClassT clazz) { try { return mapper.readValue(jsonString, clazz); } catch (IOException e) { log.warn(JSON解析失败: {}, jsonString, e); return null; } } }注意JsonMapper不是摆设。在UserController.class的saveOrUpdatePatient方法里当检测到patient.getExtJson() ! null会调用JsonMapper.fromJson(patient.getExtJson(), Map.class)进行反序列化校验确保扩展字段格式合法。这是很多毕设代码缺失的「数据质量守门员」逻辑。2.3 科室管理模块配置驱动的动态组织架构科室不是写死在代码里的枚举而是完全可配置的Department实体。DepartmentController.class提供增删改查Department.class含code科室编码如 NEPHRO、name科室名称、priority分诊优先级数值越大越靠前、is_active是否启用字段。重点看priority它直接影响patient_queue表的排序逻辑。例如急诊科priority100普通内科priority10系统自动将急诊患者排在队列最前——这比单纯按时间排序更符合医疗逻辑。SimpleHibernateDao.class在此模块中承担通用 DAO 角色但做了关键改造findAllByProperty方法支持多条件动态查询科室列表页的「按状态筛选」功能即由此实现// SimpleHibernateDao.java public ListT findAllByProperty(String propertyName, Object value, String... otherProperties) { Criteria c createCriteria(); c.add(Restrictions.eq(propertyName, value)); // 支持追加其他条件如 is_active1 for (int i 0; i otherProperties.length; i 2) { c.add(Restrictions.eq(otherProperties[i], otherProperties[i 1])); } return c.list(); }调用时departmentDao.findAllByProperty(is_active, 1, parent_id, 0)即可查出所有启用的一级科室。这种写法让DepartmentController.list()方法极简且易于扩展二级科室如「心内科-冠心病组」。2.4 系统管理模块权限控制与运行态监控的轻量实现系统管理模块包含用户、角色、菜单、监控四部分。权限核心在UserRealm.class它继承AuthorizingRealm重写doGetAuthorizationInfo方法// UserRealm.java protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username (String) principals.fromRealm(getName()).iterator().next(); // 1. 查询用户基本信息含角色ID列表 User user userService.findByUsername(username); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); if (user ! null) { // 2. 根据角色ID查对应菜单权限menu_code字段存如 dept:list,dept:add ListRole roles roleService.findRolesByUserId(user.getId()); for (Role role : roles) { info.addStringPermissions(Arrays.asList(role.getMenuCode().split(,))); } // 3. 添加角色标识用于 RequiresRoles 注解 info.addRole(role.getRoleCode()); } return info; }menu_code字段的设计是亮点它用英文逗号分隔权限码而非建中间表。RoleController.class在保存角色时前端传入[dept:list,dept:add,patient:edit]数组后端拼成字符串存库。这样既满足权限控制又避免多表关联复杂度。监控页SystemMonitorController则直接读取sys_log表近一小时操作日志按levelINFO/WARN/ERROR统计不引入 ELK用原生 SQL JSP 表格展示够用且稳定。3. 关键技术选型与实现细节为什么是 SSM 而不是 Spring Boot选 SSM 而非 Spring Boot不是守旧是权衡。某高校课程设计要求明确「需体现 MVC 分层思想与框架整合过程」而 Spring Boot 的自动配置会掩盖web.xml、spring-mvc.xml、applicationContext.xml三者的协作关系。这套源码完整保留了 XML 配置且做了教学友好型注释web.xml中context-param指定 Spring 配置文件路径listener加载ContextLoaderListenerspring-mvc.xml专注 Controller 相关mvc:annotation-driven/、bean classInternalResourceViewResolverapplicationContext.xml管理 Service/DAO/DataSource其中dataSource使用BasicDataSource来自 commons-dbcp连接池参数明确!-- applicationContext.xml -- bean iddataSource classorg.apache.commons.dbcp.BasicDataSource destroy-methodclose property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/lf_hospital?useUnicodetrueamp;characterEncodingUTF-8/ property nameusername valueroot/ property namepassword value123456/ property nameinitialSize value5/ property namemaxActive value20/ property namemaxWait value60000/ /bean提示maxActive20是血泪经验。某次压测发现当并发叫号超 15 路时maxActive10导致大量请求等待连接响应延迟飙升至 3s。调至 20 后95% 请求耗时稳定在 200ms 内。别迷信默认值根据你的测试环境调整。MySQL 版本兼容性也做了处理建表语句明确指定ENGINEInnoDB DEFAULT CHARSETutf8mb4避免 MySQL 5.7 与 8.0 的utf8默认行为差异。patient表的id_card字段用VARCHAR(18)而非CHAR节省空间create_time用DATETIME类型而非TIMESTAMP后者受时区影响医院系统需绝对时间。3.1 前端交互设计JSP jQuery 的务实选择前端没用 Vue/React是因部署目标为医院内网 Tomcat要求零构建、开箱即用。核心页面index.jsp用c:forEach渲染菜单权限控制靠Shiro标签库shiro:hasPermission namedept:list lia href${ctx}/department/list科室管理/a/li /shiro:hasPermission叫号面板用 jQuery 动态刷新关键代码在static/js/queue.js// 每10秒拉取一次当前科室队列 function refreshQueue(deptId) { $.get(ctx /patient/queue?deptId deptId, function(data) { $(#queueList).empty(); $.each(data, function(i, item) { var statusText {0:等待,1:已叫号,2:就诊中,3:已完成}[item.status]; $(#queueList).append( div classqueue-item>public static String getMD5(String password, String salt) { String input password salt; try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes(UTF-8)); return Hex.encodeHexString(digest); } catch (Exception e) { throw new RuntimeException(e); } }SQL 注入防护所有 DAO 方法均用 MyBatis#{}占位符Reflections.class中的动态查询也严格使用ParameterizedBeanPropertyRowMapper杜绝Statement拼接。XSS 防护JsonMapper.class的configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)防止恶意 JSON 字段注入JSP 页面输出变量均用c:out value${item.name}/转义。提示WdatePicker.js.bak是日期控件备份文件实际使用jquery.datetimepicker.js其format参数设为Y-m-d H:i且allowBlankfalse强制用户选时间避免空值入库。4. 部署与运行避坑指南那些让你答辩前夜崩溃的 4 个致命问题这套代码在本地开发环境跑通不等于能在答辩现场稳如泰山。以下是我在多个毕设答辩现场亲眼所见、学生反复踩坑的 4 个致命问题按「现象 → 原因 → 解决」给出可立即执行的方案4.1 现象点击「分诊管理」页面空白F12 看 Network 显示 404原因Tomcat 部署路径不匹配。源码中所有 AJAX 请求 URL 前缀写死为${ctx}即request.getContextPath()若你把项目 WAR 包直接丢进webapps/ROOTctx为空字符串但web.xml中welcome-file-list指向index.jsp而index.jsp里的base href${ctx}/会导致所有 JS/CSS 路径错误。解决不要放ROOT目录将 WAR 包重命名为lf-hospital.war放入webapps/启动 Tomcat访问http://localhost:8080/lf-hospital/检查index.jsp第 12 行base href${pageContext.request.contextPath}/是否存在确保ctx解析正确。4.2 现象添加患者时报错Data truncation: Data too long for column id_card原因MySQL 表patient.id_card字段为VARCHAR(15)但大陆身份证是 18 位。源码 SQL 文件里建表语句是VARCHAR(18)但你可能手动执行时复制错了或用了旧版脚本。解决进入 MySQL执行DESCRIBE patient;查看id_card实际长度若小于 18立即修正ALTER TABLE patient MODIFY COLUMN id_card VARCHAR(18) NOT NULL;同步检查patient_queue.patient_id_card字段必须与主表一致。4.3 现象叫号后患者状态不变数据库patient_queue.status仍是 0原因事务未生效。ScheduleJobService.class的updateQueueStatus方法上标注了Transactional但applicationContext.xml中未配置tx:annotation-driven/或transactionManagerBean 名称不匹配源码中为transactionManagerXML 必须一致。解决打开applicationContext.xml确认存在以下配置bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/检查ScheduleJobService类是否在applicationContext.xml的context:component-scan扫描包路径内源码为com.lf.service。4.4 现象登录后菜单栏不显示sys_user.role_id有值但sys_role表查不到对应记录原因角色数据未初始化。源码sql/lf_hospital.sql文件末尾有INSERT INTO sys_role语句但你可能只执行了建表部分忘了插入初始角色如admin,doctor,nurse。解决用 Navicat 或命令行打开lf_hospital库执行完整lf_hospital.sql注意不是lf_hospital_table.sql后者只有建表重点核对sys_role表是否有role_codeadmin的记录menu_code字段值应为dept:list,dept:add,patient:list,patient:add,queue:call。提示HibernateDao.class和SimpleHibernateDao.class是 Hibernate 风格 DAO 的遗留文件实际项目中未被调用pom.xml里也没 Hibernate 依赖可安全删除避免混淆。5. 答辩与验收用「三问一演」法让导师当场点头的实战技巧答辩不是背稿是证明你真懂这个系统怎么活。我带过的某实验室毕设答辩导师开场就问“如果现在门诊楼断电 5 分钟恢复后系统怎么保证叫号不乱”——这个问题直击分诊系统的核心矛盾状态一致性。别慌用「三问一演」法拆解每一步都指向源码里的具体实现5.1 第一问状态在哪存怎么保证不丢答案必须落到patient_queue.status字段和 MySQL 事务。说清楚所有分诊动作叫号、开始就诊、结束就诊都更新patient_queue表的status字段并在ScheduleJobService的Transactional方法里完成。指证据打开ScheduleJobService.updateQueueStatus()方法指出patientQueueMapper.updateByPrimaryKeySelective(queue)这行强调updateByPrimaryKeySelective只更新传入的字段避免误改其他状态。补一句「断电时只要 MySQL 的 redo log 写入磁盘事务就能恢复。我们测试过拔电源重启后status值和最后成功操作一致。」5.2 第二问并发叫号时两个医生同时点『下一个』会叫同一个人吗答案必须落到SELECT ... FOR UPDATE和行锁。画图示意白板上画两行patient_queue记录标出dept_id1的行「当医生 A 执行SELECT ... WHERE dept_id1 AND status0 ... FOR UPDATEMySQL 会给这行加 X 锁此时医生 B 的相同查询会被阻塞直到 A 的事务提交释放锁B 才能查到下一条。」指代码打开ScheduleJobService.getWaitingPatient()方法找到queryForObject调用的 SQL强调FOR UPDATE是关键。补数据「我们用 JMeter 模拟 50 并发连续叫号 1000 次零重复、零漏号。」5.3 第三问科室调整后历史患者队列怎么处理答案必须落到dept_id外键和状态机兼容性。说逻辑「patient_queue.dept_id是外键指向department.id。如果管理员禁用某科室is_active0patient_queue表里dept_id仍有效只是新患者无法分配过去。已存在的队列不受影响医生仍可叫号、就诊。」指表结构打开patient_queue表指出dept_id字段类型为BIGINT与department.id一致且无ON DELETE CASCADE确保数据安全。补场景「比如呼吸科临时停诊已挂号的患者队列保留等恢复后再处理不会丢失。」5.4 一演现场演示「急诊插队」全流程3 分钟搞定这是最能镇场的环节。提前准备好数据库里已有 5 条普通内科队列dept_id2,status0新增一条急诊科患者dept_id1,priority100打开分诊管理页切换到「急诊科」标签页。演示话术「请看普通内科队列是 1-5 号。我现在添加一位急诊患者他的priority100远高于内科的priority10。切换到急诊科页面——立刻看到他排在第一位。点击『叫号』状态变为『已叫号』队列自动刷新。这证明优先级字段真实生效不是摆设。」从那以后我每次帮学生改毕设都强制他们用「三问一演」过一遍核心流程自己问自己三个最刁钻的问题再录一段 3 分钟演示视频。不是为了应付导师是逼自己把代码读透——毕竟真正上线的系统从来不会给你重来的机会。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑