又到毕业设计季。每年我帮人看毕设遇到最多的题目种类之一就是医院信息系统电子病历系统、智慧医疗平台、门诊管理系统……基本每年都会冒出来一批。这个方向确实好写因为业务成熟、模块清晰、能发挥的空间也大。但正因如此很多人的设计只停留在“把增删改查页面做出来”的水平答辩时一被问到“你怎么设计数据模型”“并发挂号怎么处理”“病历数据结构怎么封装的”就当场卡住了。这篇文章就以基于SpringBoot的电子病历系统为例从头过一遍我自己的设计思路和实现细节需求怎么拆、模块怎么分、数据模型怎么建、权限怎么做、并发怎么控制、部署有哪些坑。如果你正在做的是智慧医疗健康管理平台、医院数字化诊疗信息系统或者综合门诊管理思路基本可以直接挪用区别只是侧重点不同。1. 需求拆解与技术选型1.1 电子病历系统到底要解决什么问题很多人看到“电子病历系统”这个词第一反应是“把病历拍照存起来”或者“做个Word在线编辑”。这其实是把核心问题理解窄了。电子病历的核心不是把纸张变成图片而是把病历从一份非结构化文档变成一套可以被查询、关联、统计和复用的结构化数据。它要记录的不只是“患者说了什么、医生写了什么”还要能把诊断、处方、检查、检验、用药、费用、随访这些环节串起来。所以拆需求第一件事不是列功能菜单而是先把医院的业务链路画出来。常规门诊流程大概是这样的患者建档科室号源分配前台挂号或线上预约候诊医生接诊医生书写病历开具处方或检查单收费药房发药检查检验出报告医生复核患者离院。后续还有慢性病随访、健康档案归档、统计报表这些衍生需求。不同角色的视角是不同的。患者要的是“少排队、病历可查、复诊方便”医生要的是“录入快、历史病历能调出来、用药和诊断有辅助提示”管理员要的是“号源可控、数据能统计、权限不出乱子”。这三个视角叠加才是一个完整的需求集合。如果只站在其中一个角色做设计后面一定会被答辩老师问住。1.2 为什么选SpringBoot而不是SSH或者微服务现在做Java后端选SpringBoot几乎是默认答案。但它到底好在哪得能说出点门道来。SpringBoot最大的优势是“约定大于配置”和“自动装配”——不用再像SSH时代那样写一堆XML配置文件内嵌Tomcat也让项目可以一键启动、独立部署。对毕业设计来说这意味着学生可以把精力放在业务逻辑和系统设计上而不是纠结“为什么数据源连接又失败了”。Java生态里SpringBoot 2.7.x Java 8/11这套组合最稳。SpringBoot 3.x虽然已经普及但它要求Java 17以上部分老版本数据库驱动和第三方工具会踩兼容性坑。如果是做新项目倒没问题但如果你是照着网上的老教程改版本不一致会浪费大量时间。建议写毕设的默认选SpringBoot 2.7.18 JDK 8稳定、教程多、答辩时也不会有太多认知代差。那要不要做成微服务我强烈不建议。毕业设计不是大厂项目单体应用完全足够。微服务带来的注册中心、配置中心、分布式事务、服务调用链每一层都是额外复杂度。更关键的是医院业务里很多场景是强事务关联的比如挂号、写病历、开处方、扣库存在单体里一个事务就能搞定拆成微服务后反而要处理分布式一致性纯属给自己挖坑。你可以在论文里写一句“系统采用模块化设计为后期微服务化预留了扩展性”既安全又能体现你有全局视野。1.3 系统模块划分与数据流设计我习惯按“前端角色端 后端业务域”的方式拆模块而不是简单地按增删改查去切。做一个医院系统至少要覆盖这几个业务域患者域建档、档案维护、就诊历史、电子病历浏览、在线预约、报告查询。门诊域科室排班、号源管理、挂号、分诊叫号、接诊记录、病历书写。诊疗域诊断记录、处方开具、检查检验申请、报告回填、转诊留观。药房域药品目录、库存管理、处方核发、过期预警。管理域用户管理、角色权限、操作日志、数据字典、统计报表。随访域慢病登记、随访计划、随访记录、健康指标趋势。数据流的核心是一张“就诊记录”或叫“病历主表”。整个系统都不是以“用户”为中心自由散落而是以“患者某一次就诊”为中心汇聚。一次就诊产生一份病历主表下面挂诊断、处方、检查、费用等子记录。这样设计的好处是无论后来做历史病历查询、统计数据、还是慢病分析都有清晰的入口和索引路径。2. 核心功能模块与数据库设计解析2.1 电子病历主数据结构设计数据库设计是电子病历系统的灵魂。很多人拿到需求就建表结果建出来一堆字段堆砌的大宽表或者把整个病历塞进一个text字段后面查询和扩展都是噩梦。我的建议是采用“主表 子表 字典表”的结构。主表就用两到三张人体基表sys_user、患者档案medical_patient、病历主表medical_record。其中medical_record应该包含患者ID、接诊医生ID、就诊科室、就诊时间、就诊类型、主诉、现病史、既往史、初步诊断、处理意见、病历状态、创建时间这些核心字段。子表至少要有诊断表medical_diagnosis、处方表prescription、处方明细表prescription_item、检查检验申请表examination_apply。诊断表可以记录西医诊断和中医诊断两个维度处方表只记录头信息明细表再记录药品ID、用法用量、天数、总量这样才算标准化的结构。比如“阿莫西林胶囊0.5g/次一天三次吃三天”在明细表里至少要拆成drug_id、dosage、frequency、days、amount这几个字段。还要配一张操作日志表。电子病历有法律效力和审计要求谁在什么时间改了什么内容必须留痕。我见过太多毕设项目没有这张表答辩时一谈到“安全性”就只能干巴巴说“我们有登录鉴权”。加上日志表之后系统会立刻显得更扎实。2.2 门诊、复诊与转诊的业务闭环电子病历不是孤立存在的它必须嵌进整个门诊流程里。我在项目里把门诊流程拆成五步号源准备、挂号锁定、医生接诊、处方支付、药房发药。每一步之间通过状态流转来衔接。号源准备阶段每个科室每天生成对应医生时段的可挂号数量。挂号阶段不只是insert一条挂号记录更关键的是要“扣减号源”。我在这里用了Redis缓存号源剩余数同时保留数据库端的一个剩余数字段。用户点击挂号后先判断缓存值是否大于0再通过一个带条件的UPDATE语句扣减数据库数据UPDATE schedule SET remain remain - 1 WHERE id ? AND remain 0。否则就会出现两个用户同时抢最后一个号都被放行的脏读问题。医生接诊阶段前端展示的是患者的排队列表。点击开始接诊后系统会创建一份medical_record主记录状态设为“接诊中”。医生录入诊断、处方后提交状态推进到“待支付”或“已完成”。转诊场景则是在主记录上打一个转诊标记生成新的目标科室挂号原科室记录保持“已转出”状态这样两个科室的医生都能看到完整病史。2.3 药品库存、处方与用药安全药品模块做得细不细很能体现一个毕设的完成度。我常用的表设计是drug目录表、drug_stock库存表、drug_stock_log操作流水表。目录表存药品基础信息比如通用名、商品名、规格、剂型、生产厂家、批准文号、零售价。库存表单独拆出来是因为同一药品可能对应多个批号不同批号的有效期和生产日期不一样把它塞在一张表里很快就会冗余。开处方时前端应该校验药品是否存在、是否停用、库存是否充足。后端在保存处方明细的同时要对库存执行扣减并写一条库存流水。注意一个细节不能用“先查库存再UPDATE”的方式因为这种查询再更新的模式在并发场景下会超卖。正确写法是用UPDATE drug_stock SET stock stock - #{num} WHERE drug_id #{drugId} AND stock #{num}如果更新影响行数为0就说明库存不足。用药安全方面可以加一些基础校验比如同一张处方里是否存在重复用药、指定药品的默认用法用量是否超出常用上限。这些规则不一定真做到临床那么专业但只要写进代码里项目高度立刻就不一样。2.4 慢病随访与健康档案很多医院类毕设会把“健康档案”做成一堆静态图片或一个PDF列表。真正要做成智慧医疗平台健康档案应该是一个动态聚合视图把患者的历次就诊信息、检查结果、检验指标、用药史、随访记录整合起来形成按时间排列的健康时间轴。慢病随访模块我建议这样设计一张chronic_disease表记录患者的慢病分类比如高血压、糖尿病一张follow_up_plan表记录计划频率比如“每三个月随访一次”一张follow_up_record表记录每次电话或线上随访的内容包括血压值、血糖值、用药依从性、生活习惯干预建议。后端用定时任务每天扫描今天应该随访的患者生成待办任务前端医生端就能看到“今天有12位患者需要随访”。这个模块的亮点在于它把单纯的“看病记录”变成了“健康管理”属于很加分的扩展点。答辩时可以说“电子病历系统不只是记录每一次就诊更重要的是通过长期随访数据来分析病情演变辅助医生做慢病管理决策。”这句话一出来项目立意就站得住了。3. SpringBoot实现中的关键环节3.1 项目骨架搭建与基础配置SpringBoot项目的骨架搭建不复杂但一些细节会影响后续开发的顺畅度。先说pom依赖一个医院系统常用的核心依赖大概是这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesJWT这里我选了jjwt 0.11.5因为老版的0.9.1依赖的javax.xml.bind在新JDK上已经没了。这种版本坑特别典型很多人的JWT报ClassNotFoundException就是从这里来的。application.yml配置里有一个很关键的点MySQL连接串必须带上时间参数。我见过太多人启动时报“The server time zone value Öйú±ê׼ʱ¼ä is unrecognized”原因就是没有指定时区。配置大概是spring: datasource: url: jdbc:mysql://localhost:3306/medical?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 servlet: multipart: max-file-size: 20MB max-request-size: 50MB mybatis-plus: global-config: db-config: id-type: assign_id configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplMyBatis-Plus里我用了assign_id主键策略它默认是雪花算法生成Long型ID。为什么要强调这一点因为很多人用AUTO_INCREMENT自增ID一旦系统出现并发或者需要分库迁移很容易撞ID或者暴露出业务规模。雪花ID虽然不是数据库自己生成的但分布式场景下它是成熟方案答辩时聊起来也更有说服力。3.2 多角色登录与权限控制医院系统的权限非常敏感。患者不能看到其他患者的病历患者看不到药品成本价科室主任可以看本科室数据但不能跨科室医生不能删除已归档病历。这些不是单一字段能做好的必须把“认证”和“授权”分层处理。我的做法是用Spring Boot写一个全局拦截器加上JWT实现无状态登录。登录时系统根据账号查出用户角色管理员、医生、护士、药师、患者。生成Token时把userType和userId放进去拦截器每次请求时解析Token把当前用户对象塞到Request的Attribute里。然后每个Controller用自定义注解控制角色访问RequireRole({DOCTOR, ADMIN}) GetMapping(/record/{id}) public Result getRecord(PathVariable Long id) { ... }自定义注解配合拦截器里的一段角色判断逻辑比在Controller里写一大串if-else要干净得多。实现的时候注意两点一是拦截器要排除登录、验证码、静态资源这类白名单路径二是每个Token要有过期时间建议2小时配合Redis做“踢下线”操作管理员禁用用户时删掉Redis里的tokenKey就能立即生效。3.3 病历检索与关键词高亮病历数据积累多了之后检索是真正的刚需。医生输入一个症状关键词要在几万份既往病历里快速找到同类病例。最简单的实现是MySQL的LIKE模糊查询“WHERE chief_complaint LIKE %胸痛%”。数据量小于一万条时这个方案没问题只要别忘记加字段类型转换为varchar即可。但如果你想在答辩时体现一点技术深度可以换一种思路用MySQL全文索引。InnoDB支持中文全文索引配合ngram分词器可以做到比较合理的搜索效果。建表时可以这样加索引CREATE FULLTEXT INDEX idx_record_fulltext ON medical_record(chief_complaint, present_illness, diagnosis_result) WITH PARSER ngram;查询时用MATCH AGAINST。需要注意ngram默认的最小分词粒度是2对于单字查询会失效但中文场景下“胸痛”“咳嗽”“高血压”这类词基本都是双字起步体验还是不错的。关键词高亮我是在后端拼好标签再返回给前端把命中的关键词用包裹起来前端只负责样式。这个方案比前端自己做高亮简单也不容易出错。还有一个隐藏坑如果用户输入了正则特殊字符必须先转义再参与高亮替换不然可能会直接把页面搞崩。3.4 全局XSS过滤与上传PDF防护安全这块XSS过滤是不少毕设容易忽略的点。所谓XSS攻击就是攻击者把恶意脚本塞进输入框里比如“ ”如果系统不做任何处理这段脚本会以HTML形式渲染到其他用户的浏览器上轻则弹窗重则窃取登录状态。医院系统里病历、备注、投诉建议这些入口太多必须做全局处理。我实现了一个OncePerRequestFilter包装Request的getParameter和getInputStream方法对参数里的HTML标签做转义。JSON请求体也要拦截因为现在前后端分离项目普遍用JSON传参。具体做法是写一个RequestWrapper读取Body后统一调用自定义的HtmlUtil.clean()把和替换成和再放行。文件上传这块更隐蔽。很多项目上传PDF后文件名或者PDF内容里可以藏一段HTML/JavaScript用户下载时如果浏览器把它当作HTML来渲染就会触发攻击。我以前踩过这个坑上传一个名为“病历.pdf”的文件下载时如果用response直接输出原文件名一旦文件名带特殊字符就会出问题。稳妥的做法是强制响应头Content-Disposition为attachment让浏览器始终下载而不是预览。文件名用URLEncoder编码避免中文文件名和特殊字符问题。文件类型做白名单校验不能只看浏览器传的Content-Type因为这是可以伪造的应该用服务端解析文件头来判断真实格式。PDF上传后建议用PDFBox提取文本做一次关键词扫描发现可执行脚本特征就拒绝入库。这个过滤器放在SpringBoot项目里不仅能覆盖接口参数还能顺带处理上传文件的附件名属于花小力气大提升的点。4. 高频报错、并发问题与部署实录4.1 病历系统开发期间的高频问题速查做这个项目的时候我整理了开发过程中最容易踩的几个问题列成一张速查表问题现象根本原因解决办法启动报serverTimezone异常MySQL连接串没指定时区连接串加serverTimezoneAsia/Shanghai插入病历时报“id is null”MyBatis-Plus主键策略不匹配全局配置id-type为assign_id或assigned_id中文乱码数据库字符集不是utf8mb4建库时指定utf8mb4连接串加characterEncodingutf8文件上传超过限制Spring默认max-file-size只有1MByml里配置multipart max-file-size和max-request-sizeJWT解析报NoClassDefFoundErrorjjwt 0.9.1依赖旧版javax升级到0.11.5对应换依赖包名接口返回JSON里有字段为nullBean序列化没有处理空值实体类加JsonInclude(Include.NON_NULL)明明加了事务却不回滚try/catch吞掉了RuntimeException在catch里手动回滚或让异常抛出到事务切面处前端跨域请求失败SpringBoot未配置跨域写全局CorsFilter允许对应域名和请求方法这些看起来都是小问题但每一个都实实在在浪费过时间。尤其是事务不回滚那个我见过一个同学在Service方法内部用try/catch包住数据操作然后把异常打了日志就返回结果插入一半失败了数据留在库里查了半天才发现是事务边界被自己破坏掉了。4.2 数据一致性与号源并发控制医院场景对数据一致性要求高号源不能超卖、药品库存不能变负数、处方不能出现半截记录。我在实现时用了几层组合拳。第一层是数据库事务。凡是一次操作涉及多张表的比如“挂号生成就诊单”“处方扣库存写流水账”必须放到同一个Transactional方法里。Spring声明式事务默认只对RuntimeException回滚如果你手写了一个异常捕获就破坏了事务。第二层是乐观锁。号源表和库存表都加一个version整数字段更新时带上“UPDATE schedule SET remainremain-1, versionversion1 WHERE id? AND version? AND remain0”。如果影响行数为0说明并发冲突返回用户“号源已被抢完”或“请重试”。这种方式比悲观锁SELECT FOR UPDATE更好因为它不会长时间锁行吞吐量高。第三层是Redis分布式锁用于更复杂的场景比如患者同时挂多个科室的号或者同一时间重复提交处方。我用的最简单的SETNX锁String lockKey lock:order: patientId; boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofSeconds(10)); if (!locked) { throw new BusinessException(请勿重复操作); }加锁之后要记得在finally里释放锁并且设置过期时间防止死锁。这一步虽然代码量不大但这对“并发控制”的问题在答辩时是非常合适的实战素材。4.3 打包部署与线上环境调优最后说部署。SpringBoot项目打jar包部署几乎是标准做法。Maven打包命令mvn clean package -DskipTests java -jar target/medical-system.jar --spring.profiles.activeprod如果是本地Windows测试注意端口占用如果是服务器部署建议在application-prod.yml里把数据库连接池、Redis连接、日志级别都重新调一遍。日志级别一定要从INFO改成WARN否则大量SQL日志会瞬间把磁盘撑满。内存配置可以这样调-Xms256m -Xmx512m这尺寸对单体毕设项目完全够用不用动不动就开2G。服务器如果只有1G内存给Tomcat预留100MB给连接池预留100MB剩下的给JVM就够了。上传文件目录也要单独处理。不要放在jar包同级目录下的随机临时目录里因为每次打包发布都可能导致文件丢失。我在项目中把上传路径配置成了可配置项服务器上固定为/data/medical/upload并且session校验和操作日志都挂在独立目录这样即使哪天把jar包重新部署患者的历史资料还在。最后再说一个个人经验。这个项目做完后我被问到最多的不是“功能怎么做的”而是“为什么这么做”。我的体会是做这类系统时每做一张表、写一个接口最好都问自己“业务上为什么需要它”。比如病历变更记录表我就是因为想到“医生写错病历后只能改不能删必须保留原始记录”这个真实需求才加的。答辩时我把这个设计意图一说导师立刻点头。这种基于业务场景的设计思考比堆砌多少高深技术都更打动人。如果你在做类似的系统建议先把门诊流程走一遍再去敲键盘方向对了后面每一步都不会白做。