尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SSM+MySQL构建医院电子病历系统:从CRUD到并发与索引优化

SSM+MySQL构建医院电子病历系统:从CRUD到并发与索引优化 简介面向Java毕业设计及课程设计场景的医院电子病历管理系统源码基于SSM框架与MySQL数据库构建适合需要快速搭建完整Web项目并撰写说明文档的计算机专业学生。系统覆盖个人中心、医院公告、用户管理、科室信息、医生管理、出诊信息、预约时间段、预约挂号、门诊病历、就诊评价及系统管理等模块完整实现从医生排班、患者预约挂号到病历记录与就诊评价的业务闭环。压缩包共2000个文件以jsp页面、java源码、xml配置、css/js前端资源及png图片为主同时包含sql脚本、md说明文档和LW文档包体约63.27MB便于本地部署与按模块学习。目前已有195人学习下载可获取完整源码、数据库设计脚本、环境配置说明及模块划分思路适合需要参考SSM整合、预约挂号逻辑与后台管理实现的毕业设计者。1. 医院电子病历管理系统为什么这类 Java 毕业设计都选 SSM MySQL医院电子病历管理系统是 Java 毕业设计里出现频率最高的题目之一几乎每个高校的软件工程、计算机科学与技术专业都能看到它。表面上看这只是一个普通的 CRUD 管理系统但电子病历本身的数据模型却比想象中复杂一份病历既要存患者基本信息、主诉、现病史、体格检查又要关联诊断、医嘱、检验检查结果关键还要记录医生操作痕迹。用 ssm 框架Spring Spring MVC MyBatis配合 MySQL 来做这类业务恰好能覆盖从数据库建模到业务层事务控制的完整链路而且网上能查到的参考资料最厚踩坑成本最低。这篇博文从一套典型的「医院电子病历管理系统」出发把 SSM 的骨架搭建、MySQL 的表设计和索引、MyBatis 动态 SQL 处理病历这种「半结构化数据」的方法、以及事务和并发控制的边界一次讲透。不管你是准备做毕业设计还是想快速上手 ssm 框架接真实业务都可以把这里的内容当一份可复现的操作手册。对于有五年以上经验的工程师重点看第 4 章和第 5 章的参数与排查思路。2. 病历系统的 SSM 骨架从 MySQL 的 E-R 图到三层分包2.1 先画 E-R 图病历不是一张表能装下的医院电子病历管理系统的核心实体至少包括患者Patient、医生Doctor、用户User用于登录认证、病历MedicalRecord、诊断Diagnosis、处方Prescription和检查检验单Examination。其中「病历」是业务主表但病历与诊断、处方、检查单都是 1 对 N 关系所以落库时需要拆成主表 明细表。常见做法是五张核心表user账号、密码MD5 加盐、姓名、角色医生/护士/管理员patient患者姓名、性别、出生日期、身份证号、联系电话、既往病史medical_record病历主表包含 visit_date就诊时间、chief_complaint主诉、present_illness现病史、past_history既往史、physical_exam体格检查、treatment_advice治疗意见、doctor_id、patient_iddiagnosis诊断明细表一个病历对应多条诊断包含 icd_codeICD-10 编码、diagnosis_name、diagnosis_type初步/确定prescription处方明细表包含 drug_name、specification规格、dosage剂量、frequency频次、days天数E-R 图里最关键的关系是medical_record与diagnosis、prescription的一对多关系以及medical_record与patient、user的多对一关系。设计表时建议把patient和user分开不要用一张表存所有角色字段否则后面做权限控制会很别扭。2.2 SSM 三层分包与整合顺序SSM 项目的分包结构决定了后期维护成本。我一般这样分com.hospital ├── controller // Spring MVC 控制器 ├── service // 业务接口 impl ├── dao // MyBatis Mapper 接口 ├── entity // POJO 实体类 ├── dto // 参数接收对象 ├── vo // 视图展示对象 ├── util // 工具类MD5、日期转换 └── interceptor // 登录拦截器SSM 整合的关键顺序是先配applicationContext.xmlSpring 容器、数据源、事务管理器、MyBatis 的 SqlSessionFactory再配spring-mvc.xml包扫描 controller、视图解析器、JSON 转换器最后在web.xml里加载这两个配置并配置 Spring MVC 的前端控制器DispatcherServlet。一个高频踩坑点是Spring 的包扫描不能覆盖controller包MVC 的包扫描不能覆盖service和dao包否则会出现「Bean 已存在但注入失败」或者「请求 404」的诡异问题。2.3 数据源与连接池的参数选择数据源用 Druid 是 ssm 项目的默认选择除了监控页面方便之外对于毕业设计演示也有直观反馈。连接池参数建议这样设# jdbc.properties jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowMultiQueriestrue jdbc.usernameroot jdbc.passwordyour_password # Druid 连接池 initialSize5 minIdle5 maxActive20 maxWait60000 timeBetweenEvictionRunsMillis60000 validationQuerySELECT 1 testWhileIdletrue testOnBorrowfalse参数说明initialSize是启动时建立的物理连接数maxActive是最大连接数毕业设计演示场景 20 足够。maxWait设为 60000 毫秒表示连接池满时等待 60 秒后抛异常避免线程无限阻塞。testWhileIdle开启后每timeBetweenEvictionRunsMillis周期会检测空闲连接的validationQuery防止 MySQL 的wait_timeout把空闲连接断开后程序拿到已失效的连接。allowMultiQueriestrue表示允许一条 SQL 里用分号写多条语句MyBatis 执行多条初始化脚本时会用到生产环境不建议开。注意serverTimezoneAsia/Shanghai必须显式声明MySQL 8.x 驱动不写这个会直接报 CST 时区识别错误。3. 用 SSM MySQL 落地病历 CRUD动态 SQL 与事务边界3.1 核心表建表语句字段类型与约束病历表的建表语句是所有查询和写入的基础。下面是一份经过调优的主表设计重点关注visit_date用datetime还是date以及主键为什么用bigint自增CREATE TABLE medical_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 病历ID, record_no VARCHAR(32) NOT NULL UNIQUE COMMENT 病历编号, patient_id BIGINT NOT NULL COMMENT 患者ID, doctor_id BIGINT NOT NULL COMMENT 医生ID, visit_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 就诊时间, chief_complaint VARCHAR(500) NOT NULL COMMENT 主诉, present_illness TEXT COMMENT 现病史, past_history TEXT COMMENT 既往史, physical_exam TEXT COMMENT 体格检查, treatment_advice VARCHAR(1000) COMMENT 治疗意见, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0草稿 1已提交 2已归档, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_patient_visit (patient_id, visit_date), KEY idx_doctor_visit (doctor_id, visit_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT病历主表;字段说明record_no是业务编号唯一约束防止重复提交visit_date用DATETIME而不是DATE因为同一患者一天可能多次就诊精确到秒可以区分status用TINYINT表示病历状态未来扩展「已归档」用 0/1/2 比真伪字符串更节省空间。version字段是乐观锁的基础第 4 章会讲它怎么处理并发编辑。联合索引idx_patient_visit支撑「查询某患者近期病历」的主高频场景回表次数有限比单独索引更高效。3.2 MyBatis 动态 SQL病历查询的条件拼接病历查询是管理系统中复杂的场景因为用户可能在搜索框输入任意组合按患者姓名、按就诊日期范围、按医生姓名、按诊断名称。在 SSM 的 Mapper XML 中用where加if拼接是最常规的写法select idselectRecordList parameterTypemap resultTypecom.hospital.vo.MedicalRecordVO SELECT mr.id, mr.record_no, mr.visit_date, mr.chief_complaint, p.patient_name, d.doctor_name FROM medical_record mr JOIN patient p ON mr.patient_id p.id JOIN user d ON mr.doctor_id d.id where if testpatientName ! null and patientName ! AND p.patient_name LIKE CONCAT(%, #{patientName}, %) /if if testdoctorId ! null AND mr.doctor_id #{doctorId} /if if teststartDate ! null and startDate ! AND mr.visit_date gt; #{startDate} /if if testendDate ! null and endDate ! AND mr.visit_date lt; CONCAT(#{endDate}, 23:59:59) /if if teststatus ! null AND mr.status #{status} /if /where ORDER BY mr.visit_date DESC /select这段 SQL 的关键点有三个第一where标签会自动去掉第一个AND避免所有条件都为空时出现WHERE AND的语法错误第二日期范围查询用和 23:59:59而不是BETWEEN因为BETWEEN在 MySQL 里对 datetime 的边界处理容易漏掉当天最后一条记录第三CONCAT(%, #{patientName}, %)用参数拼接而不是${patientName}防止 SQL 注入这是 ssm 项目里最容易掉分的地方。3.3 事务控制一次保存病历 诊断明细保存一份完整病历需要同时写medical_record、diagnosis、prescription三张表任何一张表失败都要回滚。Spring 的声明式事务用注解就能解决Service public class MedicalRecordServiceImpl implements MedicalRecordService { Autowired private MedicalRecordMapper recordMapper; Autowired private DiagnosisMapper diagnosisMapper; Autowired private PrescriptionMapper prescriptionMapper; Transactional(rollbackFor Exception.class) Override public Long saveRecord(MedicalRecordDTO dto) { MedicalRecord record new MedicalRecord(); BeanUtils.copyProperties(dto, record); record.setStatus(1); // 提交状态 record.setRecordNo(generateRecordNo(dto.getVisitDate())); recordMapper.insert(record); // 保存诊断明细 if (dto.getDiagnosisList() ! null !dto.getDiagnosisList().isEmpty()) { for (DiagnosisDTO diag : dto.getDiagnosisList()) { Diagnosis diagnosis new Diagnosis(); BeanUtils.copyProperties(diag, diagnosis); diagnosis.setRecordId(record.getId()); diagnosisMapper.insert(diagnosis); } } // 保存处方明细 if (dto.getPrescriptionList() ! null !dto.getPrescriptionList().isEmpty()) { for (PrescriptionDTO pre : dto.getPrescriptionList()) { Prescription prescription new Prescription(); BeanUtils.copyProperties(pre, prescription); prescription.setRecordId(record.getId()); prescriptionMapper.insert(prescription); } } return record.getId(); } }Transactional(rollbackFor Exception.class)的含义是方法内抛出的任何Exception及其子类都触发回滚包括自定义的运行时异常。这里要注意两点rollbackFor必须显式声明为Exception.classSpring 默认只在RuntimeException时回滚如果业务层抛出Exception的受检异常不加这个参数会导致部分写入无法回滚。上述代码中recordMapper.insert(record)之后由于 MyBatis 的useGeneratedKeys配置在insert标签里配useGeneratedKeystrue keyPropertyidrecord.getId()才能拿到 MySQL 自增生成的主键用于写诊断表和处方表的外键record_id。事务的边界就划在saveRecord方法上Controller 层调用这个方法时如果中间的循环插入出现异常Spring 事务管理器会统一回滚三张表的数据。4. 病历管理系统的常见坑时间精度、并发编辑与 MySQL 索引失效4.1 就诊时间的精度陷阱很多毕业设计代码用java.util.Date接收前端的日期字符串然后直接存入 MySQL 的datetime字段。这里最经典的坑是前端传2025-03-18 09:30经过 JSON 反序列化变成Date对象但如果使用DateTimeFormat(pattern yyyy-MM-dd HH:mm)或者 Jackson 的JsonFormat配置不一致收到的值可能变成2025-03-18 09:30:00.0或者其他格式的字符串与数据库比对时出现查不到数据的问题。更麻烦的是时分秒的精度。前端日期范围查询传一个日期2025-03-18如果后端拿它直接和visit_date比较实际上相当于2025-03-18 00:00:00而当天下午的病历根本查不出来。所以第 3.2 节的 SQL 里endDate要拼接23:59:59。MySQL 8.0 之后可以用CAST(#{endDate} AS DATETIME) INTERVAL 1 DAY - INTERVAL 1 SECOND来替代拼接效果一样但语义更清楚。解决方案是统一用LocalDateTime配合JsonFormat(pattern yyyy-MM-dd HH:mm:ss)处理前端传值实体类里全部用LocalDateTime数据库层用datetime。这样省去了Date和字符串之间各种SimpleDateFormat的转换也天然规避线程安全问题SimpleDateFormat不是线程安全的。如果项目里大量用了java.util.Date建议抽一个DateUtils统一转换不要在 Controller 里到处写new SimpleDateFormat()。4.2 乐观锁 version 字段解决医生并发编辑电子病历的编辑场景和普通表单不同一个重要特征是两名医生可能在同一个患者档案下同时编辑病历的不同部分。MySQL 的SELECT ... FOR UPDATE悲观锁可以保证数据一致但在这种低冲突、高并发的编辑场景里悲观锁把一次编辑会话从打开页面到提交完成的全部时间都纳入锁范围病人病历多的时候容易造成等待超时。之前表结构里给medical_record加了version INT NOT NULL DEFAULT 0就是配合乐观锁使用。更新时的 SQLupdate idupdateRecord UPDATE medical_record SET chief_complaint #{chiefComplaint}, present_illness #{presentIllness}, physical_exam #{physicalExam}, treatment_advice #{treatmentAdvice}, status #{status}, version version 1 WHERE id #{id} AND version #{version} /update逻辑说明执行更新前先查出当前记录的version更新时把它作为WHERE条件并同时让version version 1。如果更新影响行数为 0说明记录已经被其他人改过抛出「病历已被他人修改请刷新后重试」的提示。这个方案的优势在于事务占用时间极短只在执行UPDATE的瞬间加行锁不需要SELECT FOR UPDATE锁住整个编辑页面。对于毕业设计答辩时演示「多人同时编辑」这个细节比直接贴代码更有说服力也是客户评审老师喜欢问的点。4.3 MySQL 索引失效的排查清单病历系统数据量达到一定程度后查询变慢是必然的。这里列一个排除索引失效的清单这条清单同样适用于 ssm 框架下的其他管理系统对列使用函数会导致索引失效比如WHERE DATE(visit_date) 2025-03-18正确写法是WHERE visit_date 2025-03-18 AND visit_date 2025-03-19隐式类型转换导致索引失效比如patient_id是BIGINT但传入的是字符串1001MySQL 做了类型转换就不能走索引LIKE 以通配符开头的查询%张%无法使用普通索引可以用LIKE 张%或查询条件里配合全文索引OR连接的非索引条件会导致整个查询放弃索引改成UNION或拆成两条 SQL联合索引在visit_date上有范围查询时其后的条件无法继续走索引用EXPLAIN验证是最快的路径EXPLAIN SELECT * FROM medical_record WHERE patient_id 1001 AND visit_date 2025-03-01;观察type列从ALL变成range或refkey列显示idx_patient_visitrows明显减少说明索引生效。如果type还是ALL先检查字段类型是否匹配。另外MySQL 8.x 默认的sql_mode包含ONLY_FULL_GROUP_BY如果系统里有GROUP BY查询比如统计每个医生的接诊量要注意SELECT的列必须在GROUP BY里或者是聚合函数否则 SQL 直接报错。很多 ssm 毕业设计代码在 MySQL 5.7 能跑换到 8.0 就报这个错这是环境差异导致的高频排查点。5. 把 SSM 病历系统从「能跑」调到「能答辩」的 3 个技巧5.1 用 PageHelper 分页而不是手写 LIMIT正规的分页查询建议用PageHelper这是 ssm 项目里最常见也最省事的做法。在pom.xml引入依赖后dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency如果项目是纯 SSM 而非 Spring Boot需要在 MyBatis 配置里加插件plugins plugin interceptorcom.github.pagehelper.PageInterceptor property namehelperDialect valuemysql/ property namereasonable valuetrue/ /plugin /pluginsreasonabletrue表示当页码小于 1 或超出总页数时自动修正为第一页或最后一页前端传一个越界的页码不会报错。调用方式是在查询前写一行PageHelper.startPage(pageNum, pageSize)紧跟其后的第一条 SQL 查询自动分页不会影响其他查询。这个插件在「分页查询同时统计总数」的毕业设计场景里是标准的加分项前提是明确它的原理PageHelper.startPage使用ThreadLocal保存分页参数所以必须保证它和紧随其后的 Mapper 查询在同一个线程内执行中间不能穿插其他查询语句。5.2 说明文档LW与代码的对照验证「说明文档LW」这套资料里通常包含需求分析、数据库设计、核心流程图。有个可靠的验收技巧是把文档里的 E-R 图拿来和数据库实际表结构做字段级对照。具体操作是在 MySQL 中查询所有表的注释SELECT TABLE_NAME, TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA hospital_db;再用SHOW FULL COLUMNS FROM medical_record;查看字段注释。如果文档中的 E-R 图有某个字段而代码的建表语句缺失说明分析阶段的设计没有完整落地这是一个可以提前发现并补充的差异点。反过来代码里存在但文档里没有的表往往是后续增加的功能需要在说明文档里补一段「系统实现阶段新增表的说明」让文档和代码保持一致性。5.3 用 MySQL General Log 验证 CRUD 的真实 SQL答辩演示时最尴尬的情况是业务逻辑报错但不知道为什么或者明明点了保存却没看到数据变化。建议在演示前开启 MySQL 的通用查询日志把后台实际执行的 SQL 打出来这样任何操作都能被追溯到对应的语句SET global general_log ON; SET global general_log_file /tmp/mysql_general.log;查询日志默认输出到表或文件中执行SET global log_output table;后通过SELECT * FROM mysql.general_log ORDER BY event_time DESC LIMIT 20;就能看到最近的操作记录。这个方法的作用不只是调试它可以验证「新增病历」操作是否真的只插入了一次也可以排查前端重复提交导致的脏数据。另外把 Controller 层的日志配上logback的 SQL 打印在applicationContext.xml里给com.hospital.dao包设置日志级别为DEBUGMyBatis 会打印每条执行的 SQL 和参数。这一步配合 MySQL 的 general log基本上所有数据层的问题都能定位。答辩时展示日志文件里一条完整的INSERT语句比空口说「系统实现了病历保存」直观得多这也是五年以上工程师在做交接时最常检查的环节。本文还有配套的精品资源点击获取
返回列表