ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM电子病历系统开发实战:从表设计到事务、权限与部署

SSM电子病历系统开发实战:从表设计到事务、权限与部署 1. 电子病历系统的真实业务场景为什么它是Java课程设计里的常青树每年答辩季看到熟悉的电子病历系统选题我都能预感到这批学生里会有人最后交出来的东西是一堆只有管理员账号能登录的增删改查。这话不是嘲讽因为我第一次做SSM电子病历系统时也是这个水平。后来把需求、表结构、事务和部署琢磨明白了才意识到这个项目真正值钱的地方不在做出来而在为什么这么做。电子病历系统大概是Java课程设计里翻台率最高的题目之一。十个做管理系统的五六个都在做医院方向这倒不全是巧合。它的业务流程足够复杂能撑得起一个学期课程设计的分量又不会像电商系统那样陷入支付、库存、秒杀这些超出课堂教学范围的领域。核心理念其实就一句话让医生在电脑上完成病历的创建、编辑、检索和处方开具同时保证患者信息的安全隔离。整套系统的角色划分很清晰用一张表就能说清楚角色主要操作最关心的数据管理员维护科室和账号、重置密码用户表、科室表医生新建/修改病历、开处方、查询患者患者、病历、处方、药品护士录入患者基本信息、更新就诊状态患者表还有一个容易被忽略的点患者一般不是系统的主动使用者。很多人习惯性给患者也加一套注册登录逻辑最后把自己累死还没有业务价值。真实的门诊流程里患者信息是前台窗口录入后形成主数据的患者本人并不登录。所以做这类系统时把患者当成被管理的数据对象而不是用户来设计整个权限模型会清爽很多。从业务流程看最典型的一个完整链路是医生登录 → 查看今日待诊患者列表 → 选择患者 → 查看或新建病历 → 录入主诉、现病史、诊断 → 开具处方 → 保存。这个链路覆盖了SSM框架里几乎所有核心知识点SpringMVC的请求转发、Controller对表单参数的绑定、Service层的事务管理、MyBatis的动态SQL和结果映射以及最容易被忽略的权限拦截。为什么选SSM而不是直接上Spring Boot很多人觉得这是老师没跟上时代。我的看法恰恰相反SSM作为课程设计框架反而能逼你把东西吃透。Spring Boot把约定大于配置写进了骨头里你很难知道一个自动配置注解背后到底发生了什么而SSM需要你手写web.xml、手动装配SqlSessionFactory、自己声明事务管理器。这套过程一旦跑通你对Spring IOC容器和MyBatis原理的掌握是扎扎实实的后面学Spring Boot几乎是一路平推。再说了面试时能把SSM装配过程讲清楚的应届生比只会拉一个start-web依赖的人有说服力得多。1.1 功能边界一上来就想做完整医院系统是大忌我见过最典型的问题就是一开始把系统想得太大。有同学上来就规划了挂号、排队叫号、检验检查、收费、住院床位管理数据结构设计到第五张表就崩了。电子病历系统的合理边界应该是以病历为中心的文档管理系统外加处方辅助不是全院信息化平台。我建议功能收缩到四个模块就够用了用户管理登录、账号维护、患者管理新增、查询、编辑基本信息、病历管理新建、修改、按条件检索、处方管理药品选择、数量、保存。这四个模块就能把CRUD、联表查询、事务、权限全部覆盖而且答辩时老师问任何一张表你都能讲清楚它存在的理由。功能边界收得越紧代码质量越容易保证反而比堆了一堆半成品模块的大系统更像一个能落地的项目。1.2 这个项目在面试里的含金量说句实在话电子病历系统在面试官眼里并不稀奇但它是少数你能把业务约束讲出彩的项目。比如你可以谈病历属于敏感数据如何做到医生只能看他负责的患者这是一个典型的行级权限问题比角色权限深一个层次。再比如保存病历时需要同时写入病历主表和处方表任何一张失败都要回滚这就是数据一致性的实际场景。把这些业务约束讲清楚比简历上堆一堆用了Redis、用了MQ但追一句就露馅的东西强太多。相关的高频Java面试题比如Spring事务传播机制、MyBatis的#{}和${}区别、SpringMVC的请求生命周期在这个项目里全都能找到对应的代码位置这才是它最大的无形资产。2. SSM三件套的分工逻辑先搞清楚谁管什么再动手写代码很多教程一上来就贴配置文件看得人一头雾水。我的经验是先理清楚SSM三个框架在项目里的分工再去看配置代码所有东西都能对上号。Spring是整个项目的大管家负责Service层和DAO层的bean创建、依赖注入以及声明式事务。你的Service实现类标注Service之后Spring在容器启动时实例化它并把Mapper接口的代理对象注入进去。这就是控制反转和依赖注入的直观体现。很多学员能背出概念但一问你的项目里哪个对象是被容器管理的就哑火说明没把概念落到代码上。在这个项目里UserService、MedicalRecordService这些类就是答案。SpringMVC是接客的前台负责处理HTTP请求。DispatcherServlet拦截指定路径的URL找到匹配的Controller方法把请求参数绑定成对象执行完业务后返回ModelAndView或直接以ResponseBody返回JSON。所有Controller就是你系统的接口层医生在页面上点按钮本质就是往这些接口发了一个GET或POST请求。为了后期好维护接口路径命名建议按资源分/login、/patient/list、/medicalRecord/save、/prescription/listDrugs风格统一才好查。MyBatis是干活的大工负责SQL。它在Mapper接口和XML文件之间建立映射接口里写方法签名XML里写SQL。这里有两个核心一是由parameterType和resultType完成的参数绑定和结果映射二是动态SQL。比如病历检索页面的条件组合查询用 标签拼接条件输入框有值就加条件没值就跳过这是MyBatis最值得掌握的用法也是比JDBC手拼字符串优雅得多的关键。2.1 一个最小可跑通的SSM装配顺序我第一次配SSM时被一个细节卡了半天web.xml里两处加载顺序。ContextLoaderListener负责加载Spring根容器管的是Service和DAODispatcherServlet加载的是SpringMVC子容器管的是Controller。如果两个容器扫描的包配置打架会出现Controller已经注入了Service却报空指针的情况因为父容器和子容器各自扫描的范围重复了导致一个类被创建了两份。正确做法是根容器只扫service和dao包SpringMVC容器只扫controller包千万不要在两边都写context:component-scan base-packagecom.example/这种全量扫描。实际配置顺序大概是web.xml配编码过滤器、ContextLoaderListener、DispatcherServlet和它的映射路径/表示所有请求都进SpringMVC然后是spring-context.xml里配数据源、SqlSessionFactory、Mapper扫描最后是spring-mvc.xml里配注解驱动、视图解析器、静态资源放行。数据源用的Druid课程设计规模的参数一组常规值就够initialSize5、maxActive20、maxWait60000并发不高的情况下完全跑得动。2.2 项目工程结构别乱建这是后期不痛苦的前提强烈建议按controller、service、mapperentity和vo、interceptor、config来分包classpath下建mapper目录放XML。我见过最混乱的场景是Controller和工具类放一个包配置扫描范围只能全量扫虽然也能跑排查问题时全部是灾难现场。统一用一个根包比如com.yourname.hospital作为起始下面分模块建子包前后端资源目录分开后期打包部署时路径问题能少一半。实体类的命名也建议统一规范数据库中下划线命名Java实体用驼峰MyBatis开启mapUnderscoreToCamelCase配置后列名到属性名的映射就自动完成了不用一个个写resultMap。3. 病历数据建模是重头戏六张核心表和它们的主外键关系数据表设计是这个项目里最值得花时间的地方。表设计不好后面写SQL会疯狂联表性能差不说逻辑还绕。我总结的这套系统最常用的六张表系统用户表(sys_user)、科室表(department)、患者表(patient)、病历主表(medical_record)、处方主表(prescription_main)、处方明细表(prescription_item)再加上药品表(drug)。处方主表和处方明细表合起来才是一张完整处方这是典型的主从表设计。先看用户表和患者表。用户表里需要有用户名、密码、盐值、角色、科室ID外键角色用TINYINT存1管理员、2医生、3护士。密码字段必须存加盐后的哈希而不是明文这个在下一节展开。患者表单独建字段包括病历号、姓名、性别、身份证号、联系电话、过敏史、建档时间。病历号建议用年月日当日序号的方式生成比如20250613001这样一眼能看出建档日期还不会把自增主键暴露在外。病历主表是整个数据模型的中心。字段除了主键、患者ID外键、医生ID外键还包括主诉、现病史、既往史、初步诊断、就诊时间、状态。有三个字段极易漏掉就诊时间因为医生可能要补录昨天的病历病历状态标识是草稿还是已提交最后修改人用来做操作追溯。主诉和现病史这类长文本MySQL里用TEXT就够别定义成VARCHAR(255)然后在程序里截断这是我见过最心酸的错误。药品表和处方表的关系是处方主表记录的是哪次就诊开了什么药方外键关联病历ID和医生ID再加上开方时间处方明细表每一行记录一个药品的ID、单价、数量、用法用量。为什么要拆成主表和明细表因为一张处方里有多个药品这是典型的一对多关系不拆表就只能要么在同一行里塞多个药品一个字段逗号拼接要么拆成多行使用相同的处方ID来辨识。这两种方案看起来省事实际坑到死前者没法按药品统计后者没法描述一次处方的完整信息。我贴一下核心的两张表DDL方便对照CREATE TABLE medical_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, doctor_id BIGINT NOT NULL, chief_complaint TEXT COMMENT 主诉, present_illness TEXT COMMENT 现病史, past_history TEXT COMMENT 既往史, diagnosis VARCHAR(255), visit_time DATETIME, status TINYINT DEFAULT 0 COMMENT 0草稿 1已提交, created_time DATETIME, updated_time DATETIME, KEY idx_patient (patient_id), KEY idx_doctor (doctor_id) ); CREATE TABLE prescription_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, prescription_id BIGINT NOT NULL, drug_id BIGINT NOT NULL, quantity INT, unit_price DECIMAL(10,2), dosage VARCHAR(100), KEY idx_prescription (prescription_id) );3.1 外键要不要建我的倾向和理由大学教材里强调外键约束企业开发里反而经常不建物理外键靠程序保证逻辑关联。课程设计这个量级我建议建。理由很直接外键是防呆设计。删除一个还被病历引用的患者时MySQL会直接拒绝防止产生孤儿数据。而且答辩时老师很爱问外键你建了就有东西讲。真正不建外键的场景是大规模高并发下的性能考虑不是课程设计需要考虑的问题。反过来如果不建外键就一定要在Service层写额外的校验逻辑判断这个患者有没有病历再决定能否删除代码量反而更大。3.2 软删除和审计字段别把表设计当作一次性用品系统上线后一定会遇到这个患者信息录错了要删的需求。直接DELETE不是不行但病历数据属于医疗档案最好是逻辑删除每一张核心业务表加一个deleted字段0正常、1已删除所有查询默认只查deleted0的数据。配合created_time、updated_time两个时间字段更新时手动或通过MyBatis拦截器统一填充。这些设计虽然课程清单里没有但会让人觉得你在按真实项目标准做事。尤其是病历表我建议连作废都要留痕和deleted字段配合还应该记录作废操作人和作废原因因为医疗场景里审计追溯是刚需。4. 从登录到病历保存的完整请求链路拦截器、事务和行级权限怎么串我先把一条完整请求串起来讲。医生在页面输入账号密码点登录表单POST到/loginLoginController把参数封装成LoginForm对象调UserService里的login方法。这个方法按用户名查库取出用户后把前端传的密码加盐哈希和库里存的哈希比对。比对通过就把user对象塞进HttpSession并设置会话超时。此后每次请求进来拦截器先判断Session里有没有user没登录直接重定向到登录页已登录放行继续。这一步看起来简单但有一个关键点值得展开密码校验不能只查一次数据库。最合理的做法是用用户名查到用户以后用该用户自己的盐值去对输入密码做哈希再比对。如果全表统一用一个盐等于所有用户同一个盐黑客拿到哈希记录后跑彩虹表直接全灭。这个系统里我用的加盐方式是MD5(明文密码 用户唯一盐值)盐值是注册时生成的UUID片段。这里多说一句真实生产环境更推荐BCrypt这类自适应哈希算法课程设计用MD5加盐展示原理是可以的但自己心里得清楚它不算安全上限。4.1 Transactional只加在需要原子性的方法上病历保存是整套系统里最典型的强一致场景一次操作要往病历主表插入记录同时往处方主表和处方明细表写入多条药品数据。这三步任意一步失败前面写入的数据都应该撤销。实现方式就是在Service方法上标注Transactional(rollbackFor Exception.class)方法内部连续调用三个Mapper的insert方法任何一步抛出异常Spring的AOP代理就会让事务回滚数据库保持一致。这就是java怎么保证数据一致性的教科书式答案数据库事务。很多人把一致性等同于写完再查一次那是最底层的兜底正确思路是让一组操作要么全部成功要么全部失败。这里还要注意事务放在Service层而不是Controller层。理论上注解加在哪都行但放在Controller会导致事务范围过大把参数校验和JSON序列化都包进事务白白占用数据库连接。事务必须只包住真正的数据库写操作这是我建议的基本原则。普通查询方法默认不加事务。查询方法加了事务反而会占用连接、降低并发能力只有写操作组合才值得用事务。数据库层面新建表时确认MySQL引擎是InnoDB默认的MyISAM不支持事务课程设计里有人用错引擎文件页上显示事务不生效却怎么都查不出来最后发现是建表时用了老默认引擎。4.2 行级权限医生凭什么只能看自己的患者电子病历系统的权限控制比管理员、医生、护士三个角色要多想一层。角色权限解决的是能不能进这个功能而病历数据还需要解决能看到哪些行。常见做法是医生查询患者列表时Service层从Session拿到当前登录医生ID拼进SQL的where条件比如WHERE patient.doctor_id #{currentDoctorId}。这样即使医生手动构造请求URL也拿不到别人的患者数据因为过滤发生在数据库层而不是前端隐藏。这是我在这类项目里最想强调的一个点。太多课程设计只做了菜单级别的权限页面隐藏了就以为安全实际上只要有人用Postman发一个POST请求就能绕过。真正的数据集隔离必须在SQL层完成前端控制只是用户体验的一部分。实现上可以做一个BaseService基类注入一个getCurrentUserId()方法从Session取值所有涉及医生维度的查询都强制拼上这个条件从代码规范上杜绝漏写的可能。4.3 前端表单验证是提效工具后端校验才是安全底线病历里必填的主诉、诊断这些字段前端用JS做非空校验体验很好。但攻击者完全可以绕过前端直接POST所以Service层还要再校验一遍字段为空、患者ID不存在、处方数量大于库存这些都要在Java代码里拦截。我习惯在Controller里先做参数合法性判断失败就返回统一的错误信息对象{code: 400, msg: 主诉不能为空}。图省事只做前端验证的同学演示时确实没事但答辩老师只要来一句我直接发个请求试试就答不上来。数据校验永远要做两份一份给人一份给机器。5. 我在这类项目里踩过的坑事务失效、日期序列化、分页插件冲突的完整复盘SSM项目看起来都是增删改查但集成细节出错以后排查起来一点不轻松。下面的坑是翻车率最高的我挨个讲下原因和解决过程。第一个坑是事务不生效。症状是保存病历时报错但之前插入的数据还在库里。原因通常有三类一是Service方法被同类内部调用this.xxx()这种方式跳过了Spring代理Transactional就失效了二是事务管理器没配或者Mapper扫描和事务切点包不一致事务注解没人处理三是表用了MyISAM引擎。排查路径建议先看Spring启动日志有没有加载事务注解相关的BeanPostProcessor再确认切入点包名最后用SHOW CREATE TABLE看引擎。最直接的办法是做一个测试接口故意抛异常观察数据是否回滚一次就能定位是哪一类。第二个坑是LocalDateTime序列化成一串数组。SpringMVC返回JSON时默认用Jackson对JSR310时间类型支持需要额外注册JavaTimeModule。没配置的话接口返回的病历时间字段会变成[2025,6,13,10,30,15]这种数组。解决方案我统一用Jackson配置引入jackson-datatype-jsr310和jackson-datatype-jdk8然后自定义ObjectMapper注册JavaTimeModule并指定LocalDateTime的输出格式yyyy-MM-dd HH:mm:ss。如果不做JSON配置至少也要在实体类的时间字段加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)不然前端拿到的数据完全没法直接展示。第三个坑是PageHelper分页插件版本冲突。如果用了PageHelper 5.x配MyBatis 3.4经常出现ClassNotFoundException: net.sf.jsqlparser.statement.select.Select。原因是PageHelper 5.x的解析依赖jsqlparser而pom没有显式引入对应版本Maven拉了一个和它版本不匹配的老jar。解决方式要么明确引入配套的jsqlparser依赖要么干脆换手写分页——先执行COUNT(*)查总数再LIMIT #{offset}, #{pageSize}取数据。课程设计数据量不大手写分页完全够用还能绕开插件冲突。手写分页唯一要注意的是count语句和查询语句都要执行事务里注意别把count结果也包进去。第四个坑是中文乱码这玩意儿能把人逼疯。前端表单提交的数据到Controller变成乱码或者返回JSON时页面显示乱码根因往往分散。排查要按三层来JSP页面本身用UTF-8HTML里声明meta charsetUTF-8web.xml里的CharacterEncodingFilter必须配encodingUTF-8和forceEncodingtrue而且Filter要排在最前面因为Tomcat默认编码是ISO-8859-1MySQL连接URL要带characterEncodingutf8。三处任有一处漏配都会在某一环节出现乱码。我折腾最久的一次是连接串配了UTF-8但服务器字符集是utf8mb4生僻字存不进去最后用SHOW VARIABLES LIKE character%查出来是排序规则不一致。第五个坑是静态资源和视图路径的404。SpringMVC把/映射给DispatcherServlet之后CSS、JS、图片全会被拿去走Controller查找找不到就是404。要么在spring-mvc.xml配mvc:default-servlet-handler/放行静态资源要么把静态资源放webapp/static下用mvc:resources location/static/ mapping/static/**/映射。视图路径方面InternalResourceViewResolver默认前缀是/WEB-INF/views/Controller返回字符串后和prefix、suffix拼成JSP路径。这个路径拼错会直接404而且Tomcat日志里不报任何异常只能一步步打印路径排查。换成谁都可能在这种奇怪问题上耗掉整个晚上。最后把五个坑汇总成一张速查表方便你排错时对照坑位典型症状根因快速定位方式事务失效报错但数据仍在库同类自调用、MyISAM、切点错误故意抛异常观察是否回滚日期序列化JSON时间变成数组Jackson没注册JSR310模块直接看接口返回JSONPageHelper冲突ClassNotFoundExceptionjsqlparser版本不配套查pom依赖树中文乱码表单数据乱码过滤器顺序错、连接串漏配分段打印编码视图/静态资源404页面或CSS加载不出来视图路径拼错、静态资源没放行打印最终拼接的路径5.1 一个少有人提但很实用的排查技巧所有SSM疑难杂症我建议第一步先确认请求到底进没进Controller。很多问题其实是请求根本没到Controller或者到了Controller但Service层异常被吞了。在Controller方法第一行加log.info(xxx method start)在Service方法第一行也加MyBatis那边配置打印SQL整个请求链路就有据可查了。这个方法笨但确实能把定位时间从几小时压缩到十几分钟。经常看到有人在Stack Overflow上贴了一大段配置却说不清异常日志打印到了哪一层这种提问信息量为零。自己先把链路日志打全通常问题已经解决了一半。6. 部署和答辩准备Tomcat版本、JDK编译路径与上线前自查清单课程设计验收时最让老师头疼的不是功能缺了什么而是学生本地能跑、换台机器就起不来。这类问题基本和JDK版本、Tomcat版本、数据库连接串三个因素有关。先说JDK。很多同学开发机装的是JDK 17项目编译目标却写的是8或者反过来。IDEA里最常见的报错是java: 警告: 源发行版 17 需要目标发行版 17。这意思是项目编译级别和配置不一致去Project Structure里看Project SDK和Modules的Language level是否一致Maven项目再看pom里maven.compiler.source和target的值。SSM老方案我建议统一用JDK 8或11编译Tomcat用8.5或9.0。Tomcat 9以上对老项目的Servlet版本和JSP支持有变化没必要为了赶新冒兼容性风险。MySQL驱动用8.x的com.mysql.cj.jdbc.Driver连接串记得加serverTimezoneAsia/Shanghai否则有的机器上会直接报时区异常。打包部署建议用Maven打war包mvn clean package -DskipTests把war丢到Tomcat的webapps目录下启动。一个容易忽略的点pom里的resource配置负责把src/main/resources下的文件打进去但Mapper XML如果放在src/main/java目录下Maven不会默认打包需要显式加一段resource配置把**/*.xml也带上否则打出来的war包里没有XML运行时直接报Invalid bound statement (not found)这类问题因为本地IDEA编译过所以本地不报错换到Tomcat就崩。6.1 上线前的20分钟自查我每次答辩演示前都会按这份清单过一遍数据库服务是否启动、连接串里的IP是否指向本机而不是课堂上白板机写死的地址、Tomcat启动参数是否加了-Dfile.encodingUTF-8、Session超时配置是否合适默认30分钟够用、初始账号能不能登录、演示数据是否准备好。最后一条最容易被忽略现场临时录入一个患者再跑到检索页搜不到体验直接崩塌。我都会提前准备两个患者、三份病历、一张处方演示时点击流畅展示效果比现场临时造数据好得多。如果演示过程需要切换网络环境数据库连接串里写死IP还要记得改成127.0.0.1不然离开实验室就启动失败。6.2 如果还想做得更出彩Word导出和图表主体功能做完还有富余时间我建议加一个病历导出Word功能这是技术含量不高但观感极好的加分项。用Apache POI的XWPFDocument能直接生成Word文档把病历文本按段落写入再做一个表头就算一个能用的导出。至于常见的追问java poi word能生成图表吗——POI确实提供XWPFChart对Word图表的部分支持但版本要求较新且偏实验性质更稳的做法是把图表先用JFreeChart或者前端ECharts渲染成图片再通过POI把图片插入Word段落里。这样导出的病历既包含病历正文又包含检查图表和真实医院病案导出文档的形态更接近。这个功能做一个简单版本半天就够但答辩观感完全不同它把管理系统变成了能产出文档的系统更贴合医疗场景。导出时记得处理中文字体Word默认字体在PC上很可能不显示中文要显式设置中文字体名。我做这个项目时最大的感受其实是技术栈只是工具真正拉开差距的是对业务场景的理解和细节的敬畏。同一个SSM电子病历系统有人做出来是增删改查的demo有人做出来是处处有约束的成品差别往往就在有没有想清楚那些业务语言病历只能标记作废不能物理删除、处方保存必须和病历同事务、医生只能看自己的患者。把这些业务语言翻译成技术实现这套项目才算真正落地了而不只是跑通了。
返回列表