简介一套基于SSM与MySQL的陆丰市医院分诊管理系统完整源码面向需要学习Java Web项目开发或毕业设计的高校学生、初级程序员及医疗信息化爱好者。系统覆盖医生线上分诊叫号、患者信息在线编辑与统计、科室管理等核心功能并附带系统管理与系统监控模块。分诊管理菜单下细分分诊管理、患者管理、科室管理三个子菜单医生可查看当前患者情况、核对所选科室、确认是否完成就诊并在线叫号患者管理支持新增患者新增后自动出现在分诊列表科室管理可灵活添加院内科室。整套代码包含SSM后端、前端页面与MySQL数据库导入即可运行。资源包内共2000个文件以png图片、js脚本、jar依赖、class编译结果、java源码、css样式、jsp页面和sql脚本为主另有html、xml、gif等辅助文件整体约43.65MB目录结构清晰便于分别查阅后端逻辑、前端界面与数据库初始化脚本。目前已有286人浏览学习。通过该项目可以熟悉SSM框架的整合方式、MyBatis操作MySQL的持久层写法以及医院分诊场景下的业务流程与界面设计既能作为课设或毕设的参考实现也可在此基础上进行二次开发。1. 医院分诊管理系统一套能跑通线上叫号的 SSM 完整工程先想一个场景患者在前台登记完护士拿着纸质单子走到诊室门口喊名字医生在诊室里一边看诊一边还得记谁先来谁后来。这套源码解决的就是这个场景里的全部麻烦——它把分诊、叫号、患者信息维护、科室管理、系统权限收进同一个 Web 系统里医生在电脑上点一下叫号大屏或诊室端就按顺序提示下一位患者。它适合两类人一类是刚学完 SSM 想找个完整可运行项目练手的开发者另一类是接医院信息化相关私活、需要一套能演示能二次改造的基础工程。整个系统由 Spring SpringMVC Shiro MyBatis 组合搭成前端是 JSP 加 jQuery 和 WdatePicker 日期控件数据落在 MySQL导入后直接能跑通属于典型的 java ssm mysql 程序设计源码。2. 技术栈与工程结构SSM 后端、JSP 前端、MySQL 数据库的整体布局2.1 技术选型为什么这套工程还在用 SSM 而不是 Spring Boot医疗信息化这类项目有一个特点存量系统多、布点分散院方长期维护的工程师更熟悉传统三层结构。所以这套项目没有赶 Spring Boot 的时髦而是老老实实走 SSM 路线——Spring 负责 Bean 管理和事务SpringMVC 负责路由和参数绑定MyBatis 负责 SQL 映射Shiro 负责登录认证与权限控制Quartz 负责定时任务调度。这套组合在今天看起来不算新但它有一个非常实际的好处分层边界清楚每个请求从 Controller 到 Service 再到 Mapper 是一条直线debug 时追链路很舒服。另外在看源码包时你会发现一个有意思的细节包里有 HibernateDao.class 和 SimpleHibernateDao.class 这样 Hibernate 风格的工具类同时又挂着 MyBatis 的 mapper 配置。这不是写错了而是早期工程从 Hibernate 迁移到 MyBatis 时留下的历史遗留属于杂交代码。阅读时碰到这两个类不要困惑它们不参与主业务流只是被少部分老接口引用着改造时可以逐步摘掉。2.2 源码包里的类文件哪些是框架代码哪些是业务代码拿到压缩包后先别急着导入打开看一圈 class 文件能大致判断出工程里各个模块的职责。我把正文里出现的类文件按功能归了一下类类文件职责归属模块User.class / UserController.class用户实体与用户管理接口系统管理RoleController.class角色管理与授权接口系统管理UserRealm.classShiro 的认证与授权核心实现权限框架JsonMapper.classJSON 序列化与反序列化工具公共工具Reflections.class反射扫描工具常用于权限注解扫描公共工具HibernateDao.class / SimpleHibernateDao.class老版 DAO 封装历史遗留ScheduleJobService.class定时任务服务系统监控WdatePicker.js.bak日期控件备份文件前端资源其中 UserRealm.class 是理解整个权限链路的关键。Shiro 在拦截到请求后会调用 UserRealm 里的 doGetAuthenticationInfo 做登录认证再通过 doGetAuthorizationInfo 加载当前用户的角色和权限码。这套工程里权限直接落在角色上通过 sys_user、sys_role、sys_user_role 三张表关联改动权限只需要往角色上挂权限码不用碰代码。2.3 目录结构SSM 工程里包和资源的常见组织方式我一般会把这类工程导入后先拉一遍包结构确认 Controller、Service、Mapper 的分层位置再去找前端页面。典型的组织方式是src/main/java ├── com.hospital.triage │ ├── controller # 分诊管理、患者管理、科室管理、用户管理 │ │ ├── TriageController.java │ │ ├── PatientController.java │ │ └── DeptController.java │ ├── service # 业务接口与实现 │ ├── dao # MyBatis Mapper 接口 │ ├── entity # 患者、科室、分诊记录、用户实体 │ ├── mapper # Mapper XML 映射文件 │ ├── shiro # UserRealm、SessionManager │ ├── util # JsonMapper、Reflections 等工具 │ └── model # 返回结果封装、分页对象 src/main/resources ├── spring-context.xml # 数据源、事务、MyBatis 配置 ├── spring-mvc.xml # 注解驱动、视图解析 └── mybatis-mapper/ # 各表 SQL 映射 src/main/webapp ├── WEB-INF ├── views # JSP 页面 ├── static # JS、CSS、WdatePicker 组件看目录时注意 controller 包里每个类只对应一个业务模块不要在里面堆公共逻辑。比如 PatientController 只处理患者增删改查TriageController 只处理分诊和叫号这样后面做二次开发时加一个接口只需要动一个 Controller、一个 Service、一个 Mapper不牵扯其他模块。2.4 运行环境与准备跑这套工程不需要很新的环境我建议用 JDK 1.8 Maven 3.6 Tomcat 8.5 MySQL 5.7 这套组合。在 IDEA 里导入后先让 Maven 把依赖拉全然后改数据库连接最后部署 Tomcat 启动。注意项目是 Maven 多模块还是单模块要看具体目录这个工程是单模块结构导入方式选Import as Maven Project即可。编码统一设成 UTF-8否则 JSP 页面中文会乱码。3. 核心模块拆解从患者建档到分诊叫号的完整业务链路3.1 患者管理先有患者才能分诊整个系统的业务起点是患者建档。流程上护士先在患者管理页面点添加录好姓名、性别、年龄、联系电话、身份证号等信息保存后这条患者记录进入患者表。分诊管理页面读取的是同一张患者表所以新患者保存后立刻能在待分诊列表里看到这就是摘要里说的添加完毕后在分诊管理里就能看到该患者。患者添加接口的 Controller 写法很典型Controller RequestMapping(/patient) public class PatientController { Autowired private PatientService patientService; RequestMapping(value /add, method RequestMethod.POST) ResponseBody public String addPatient(RequestBody Patient patient) { // 新增前校验身份证号是否已存在避免重复建档 if (patientService.checkIdCardExists(patient.getIdCard())) { return 该身份证号已存在请勿重复添加; } patient.setCreateTime(new Date()); patient.setStatus(0); // 0表示待分诊1表示已分诊2表示已完成 int result patientService.insertPatient(patient); return result 0 ? success : failed; } }这段代码里最关键的是 status 字段它承担了患者整个就诊流程的状态标记。0 是刚建档还没分诊1 是护士选了科室完成分诊2 是医生叫号并看诊结束。后边的分诊管理列表和叫号操作全部依赖这个字段做筛选和流转所以二次开发时如果要加已叫号未就诊这类中间状态直接在这个字段上扩展即可。Service 层加接口做的事情很标准先判重再补全创建时间和默认状态然后走 MyBatis 的 insert 落库。事务注解 Transactional 加在 Service 实现类上保证 insert 失败时不会留下半截数据。我没看到源码里具体的事务配置方式但按这类工程的惯例会在 spring-context.xml 里统一开启注解驱动事务管理切面点在 service 包下。3.2 分诊管理科室绑定、状态流转与在线叫号分诊管理是整个系统最核心的模块它同时承担三件事展示待分诊患者列表、为患者指定科室、医生在线叫号。分诊列表的查询逻辑是多表联查患者表关联科室表把科室名称和当前状态一起查出来RequestMapping(value /triage/list, method RequestMethod.GET) ResponseBody public MapString, Object triageList(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int limit) { MapString, Object result new HashMap(); // 底层按 status 2 过滤只显示待分诊和已分诊但未完成的患者 ListTriageVO list triageService.queryTriagePage(page, limit); int total triageService.countTriage(); result.put(code, 0); result.put(count, total); result.put(data, list); return result; }这里返回的 TriageVO 是一个视图对象里面包含患者姓名、选择的科室、分诊时间、是否完成就诊等展示字段。它和实体类的区别在于 VO 是给前端页面定制的聚合对象不会把数据库所有字段都暴露出去。列表查询的过滤条件也值得注意状态不是只看 0而是把 0 和 1 都捞出来保证已经分诊还没看诊的患者继续留在列表里方便医生按顺序叫号。叫号动作本质上是把患者状态从 1 改成 2同时记录叫号时间。实现上是一个简单的状态更新但业务含义明确——哪个医生在什么时间叫了哪个患者后续统计模块全靠这条记录RequestMapping(value /call, method RequestMethod.POST) ResponseBody public String callNext(RequestParam int triageId) { // 加锁防止两个医生同时对同一个患者叫号 boolean locked triageService.lockPatient(triageId); if (!locked) { return 该患者已被其他医生叫号; } TriageRecord record new TriageRecord(); record.setTriageId(triageId); record.setCallTime(new Date()); // 更新患者状态为已完成同时写一条分诊记录 triageService.updateTriageStatus(triageId, 2); triageService.insertCallRecord(record); return 请患者携带就诊卡到 3 号诊室; }叫号这个操作在并发场景下容易出问题两个医生同时对一个患者叫号会出现一人被叫两次的冲突。所以源码里专门做了 lockPatient 这一步用数据库行锁或者 Redis 分布式锁把患者记录锁住后到的请求直接返回冲突提示。我自己的项目里一般用乐观锁解决给患者表加一个 version 字段更新时带上版本号更新影响行数为 0 就说明被别人抢了。这套源码具体用哪种锁机制可以看 service 实现但思路是一致的——叫号必须先锁后更。3.3 科室管理分诊的前置配置科室管理的功能相对简单就是维护医院里一共有哪些科室分诊时下拉框的数据源从这里来。一个科室字段包含科室编号、科室名称、所属楼层、是否启用等信息。新增科室的逻辑和患者新增类似但多一个停用功能。我平时会建议在科室表里加 is_enabled 字段停用的科室在分诊时不再出现在下拉框里但不会影响历史分诊记录的显示避免老数据变成无科室状态。4. 数据库设计与 Mapper 层分诊记录如何联动患者和科室4.1 核心表结构整个系统最核心的表是患者表、科室表和分诊记录表。建表语句如下三张表通过外键逻辑关联分诊记录表同时引用患者和科室CREATE TABLE t_patient ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, id_card VARCHAR(18) DEFAULT NULL COMMENT 身份证号, patient_name VARCHAR(50) NOT NULL COMMENT 患者姓名, gender TINYINT DEFAULT 0 COMMENT 0男 1女, age INT DEFAULT NULL COMMENT 年龄, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, status TINYINT DEFAULT 0 COMMENT 0待分诊 1已分诊 2已完成, create_time DATETIME DEFAULT NULL COMMENT 建档时间, PRIMARY KEY (id), UNIQUE KEY uk_id_card (id_card) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT患者信息表; CREATE TABLE t_department ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, dept_code VARCHAR(20) NOT NULL COMMENT 科室编号, dept_name VARCHAR(50) NOT NULL COMMENT 科室名称, floor VARCHAR(20) DEFAULT NULL COMMENT 所在楼层, is_enabled TINYINT DEFAULT 1 COMMENT 1启用 0停用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT科室表; CREATE TABLE t_triage_record ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, patient_id INT NOT NULL COMMENT 患者ID, dept_id INT NOT NULL COMMENT 科室ID, dept_name VARCHAR(50) DEFAULT NULL COMMENT 科室名称快照, call_time DATETIME DEFAULT NULL COMMENT 叫号时间, status TINYINT DEFAULT 1 COMMENT 分诊状态, PRIMARY KEY (id), KEY idx_patient_id (patient_id), KEY idx_dept_id (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT分诊记录表;这里有两个设计上的细节值得展开。一个是分诊记录表里专门存了 dept_name 快照字段为什么不直接靠 dept_id 关联科室表拿名字因为如果科室以后改了名字历史分诊记录里的科室名也会跟着变统计报表里这个科室当时接了多少人就没法看了。快照字段在业务表里属于典型的空间换时间、换稳定性做法。另一个是患者表身份证号加了唯一索引。医院场景下一个人只能建一次档重复建档会直接导致分诊列表里出现两个一样的患者所以这个唯一约束在数据库层面做掉了。4.2 关键 SQL分诊列表联查与叫号更新Mapper XML 里的 SQL 最能看出一个系统的真实水平。分诊列表用的联查思路是主表在分诊记录上左右关联患者和科室而不是反向去患者表里查select idqueryTriagePage resultTypecom.hospital.triage.model.vo.TriageVO SELECT r.id AS triageId, p.patient_name AS patientName, p.gender AS gender, p.age AS age, r.dept_name AS deptName, r.call_time AS callTime, r.status AS status FROM t_triage_record r LEFT JOIN t_patient p ON r.patient_id p.id WHERE r.status lt; 2 ORDER BY CASE WHEN r.status 0 THEN 0 ELSE 1 END, r.id ASC LIMIT #{offset}, #{limit} /select这段 SQL 有几个值得抄的地方。ORDER BY 里用 CASE WHEN 做排序优先级把还没分诊的记录排前面已经分诊在排队的排后面同状态下再按 id 升序天然就是按建档先后顺序排队等于不用额外维护一个队列字段。LIMIT #{offset}, #{limit} 是 MyBatis 里手写分页的最常见写法offset 从 0 开始limit 是每页条数。叫号更新的 SQL 更简单但要特别注意 WHERE 条件必须带上旧状态update idupdateTriageStatus UPDATE t_triage_record SET status #{newStatus}, call_time NOW() WHERE id #{triageId} AND status #{oldStatus} /updateWHERE 里带 status #{oldStatus} 是个好习惯它把状态更新的前置条件放到了 SQL 层更新影响行数为 0 时说明状态已经被别人改过Service 层再决定要不要报冲突。这比先查一次再更新要稳得多天然防并发覆盖。4.3 用户与权限的表设计系统管理模块涉及用户和角色表结构是三张表sys_user 存账号密码、sys_role 存角色名、sys_user_role 存关联关系。密码字段的存储方式是 Shiro 框架的典型做法——不加盐的 MD5 或者加盐 MD5 取决于源码里 UserRealm 的匹配器配置我见过的大部分 SSM 老工程用的是 MD5 加盐盐值就是用户名。这套源码具体用的哪种可以在数据库里看密码字段的格式判断32 位纯 MD5 是不加盐带长随机串的是加盐。sys_user_role 关联表还隐含了一个权限细节——角色是扁平的没有角色层级。这意味着一个用户如果想同时拥有分诊护士和医生两种角色的权限就必须在关联表里配两条记录。UserRealm 在授权时会把多个角色的权限码取并集。这种设计简单粗暴但确实够用医院内部角色数量通常不超过十来个。5. 跑通与排查从导入源码到首次叫号的 5 个踩坑记录这套源码整体不算复杂但跑起来的过程中有几个坑是几乎每个导入者都会撞到的我按现象 → 原因 → 解决的顺序列出来能少走不少弯路。5.1 JSP 页面 404视图解析器前缀配置不一致现象Tomcat 启动不报错登录页能开但点进分诊管理菜单后页面 404。原因spring-mvc.xml 里 InternalResourceViewResolver 配置的 prefix 是 /WEB-INF/views/但实际 JSP 文件被放在了 /views/ 目录下前后缀对不上SpringMVC 找不到视图。解决把 JSP 文件统一挪到 /WEB-INF/views/ 下面或者改 prefix 指向实际目录。我一般习惯统一放到 WEB-INF 下因为 WEB-INF 目录默认不能通过 URL 直接访问相当于给页面加了一层防直接访问的保险。5.2 登录界面无响应Shiro 拦截路径把静态资源拦了现象页面样式全丢登录请求发出去一直转圈控制台看到 /static 路径的请求全部被拦截。原因Shiro 的过滤器链里把 /** 直接设置成 authc静态资源也被拦在里面未认证请求拿不到 CSS 和 JS。解决在 ShiroConfig 里放行静态资源配置顺序很关键anon 规则必须写在 authc 前面MapString, String filterChain new LinkedHashMap(); filterChain.put(/static/**, anon); // 静态资源匿名访问 filterChain.put(/css/**, anon); filterChain.put(/js/**, anon); filterChain.put(/login, anon); filterChain.put(/**, authc); // 其余请求需要登录5.3 分诊列表查不出数据status 字段的初始值没写对现象患者添加成功后分诊管理页面列表是空的数据库里明明有记录。原因添加患者接口没给 status 赋值而数据库字段默认值设置了 DEFAULT 0在 MyBatis 的 insert 语句没写这个字段时新插入的记录 status 是 NULL而列表查询条件是 status 2NULL 参与比较永远不成立。解决在实体类里给 status 初始化成 0或者在 insert SQL 里显式写上 status 字段。这两种方式我推荐前者因为实体类初始化在单元测试里更容易验证。5.4 日期控件不加载WdatePicker 资源路径带上了 bak 后缀现象患者管理页面的出生日期输入框点击没反应页面报 WdatePicker.js 找不到。原因源码包提供的是 WdatePicker.js.bak 这个备份文件原开发者在替换日期控件的时候把原文件改了后缀但页面 script 引用的还是 WdatePicker.js。解决复制一份 WdatePicker.js.bak 改名为 WdatePicker.js放到同一目录下。这个坑每次导入都会遇到我一般第一步就先检查 WebContent 下的 js/ 目录有没有这个问题。5.5 叫号后状态没变化Service 事务回滚把更新吞了现象点击叫号按钮提示成功但刷新后分诊记录状态还是 1没变成 2。原因叫号方法里先调用了 lockPatient这个方法内部开了新事务做了行锁紧接着的 updateTriageStatus 在同一个事务上下文里但因为 lockPatient 的事务已经提交外层事务把它当成只读状态变更没有真正提交。解决做法是把叫号相关的两步合并到一个事务方法里或者把锁提前到入口处避免嵌套事务。这也是我在前面代码示例里把 lock 和 update 分开写但强调要合并的原因——实际项目里这种状态更新被嵌套事务吞掉的情况太常见了排查起来还特别隐蔽。6. 二次开发思路给分诊系统加多诊室队列与统计报表拿到这套系统并跑通之后多数人的需求不只是看效果而是要往里加东西。我挑两个最有代表性的改造点说一下思路。第一个改造是单队列变多诊室。现在系统里一个科室对应一个队列叫号顺序是全科统一排。但现实中一个科室有好几个诊室每个医生各排一队。改造的核心是把 t_triage_record 表加一个 doctor_id 字段分诊时下拉选择科室后再选接诊医生叫号列表按 doctor_id 过滤每个医生只看到分配给自己的患者。SQL 上只需要把查询条件从 WHERE dept_id ? 改成 WHERE doctor_id ?再按 call_time 排序。这个改动涉及 Controller 加一个参数、Mapper 加一个条件、前端分诊页面加一个医生下拉框整体工作量不大但对业务价值的提升非常明显。第二个改造是就诊统计报表。现在的分诊记录包含叫号时间但缺少患者从分诊到叫号的时间差计算。如果要做一个每科室平均候诊时间的报表在查询时把 wait_minutes 字段在 SQL 里直接算出来SELECT dept_name, COUNT(*) AS total_count, ROUND(AVG(TIMESTAMPDIFF(MINUTE, create_time, call_time)), 1) AS avg_wait_minutes FROM t_triage_record WHERE call_time IS NOT NULL GROUP BY dept_name有了这个数据可以进一步做科室负荷分析、高峰时段分布甚至可以做一个简单的趋势图页面。这套工程我前后拆了两遍第一遍只顾着把功能跑通没注意事务和锁的问题第二遍才真正理解它状态流转的设计。从那以后我不管接手哪套 SSM 工程都强制自己先画一张状态流转图 表关系图再动代码因为这类老工程真正的复杂度从来不在单点功能上而在状态与状态之间的边界里。希望帮到你。本文还有配套的精品资源点击获取