拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

中小型医院管理系统拆解:SSM与Django双技术栈实践

中小型医院管理系统拆解:SSM与Django双技术栈实践 中小型医院管理系统这个标题乍一看像是毕业设计仓库里最常见的那类“管理系统全家桶”但真正把这套系统从需求分析跑到上线调试你会发现它其实是一条完整的医疗业务链路覆盖了挂号、门诊、药房、住院、结算这些核心场景。这篇文章我想从实际项目交付的角度把整套系统的技术选型、模块设计、数据库关系、关键代码实现和踩坑记录完整拆一遍适合正在做课程设计或毕业设计的计算机专业学生也适合刚入行想做医疗信息化项目的Java开发。项目本身同时提供了JavaSSM和Django两套技术栈的实现源码配合论文文档就是标题里的LW、调试文档和视频讲解是一个相当经典的“双技术栈”交付形态。1. 项目整体拆解这套系统到底在解决什么问题1.1 需求侧的痛点中小医院的管理通病不管是校医院、社区卫生院还是三四线城市的中型综合医院只要规模没大到上HISHospital Information System大厂产品管理上基本都逃不过几个共同的坑患者信息靠手写病历本医生换班后历史就诊记录基本断档挂号窗口排队严重号源情况只有现场问了才知道药房库存和门诊开药是两套台账月底盘点怎么都对不上收费靠人工记流水财务对账能对到怀疑人生。这套中小型医院管理系统的设计目标就是把上面这些线下碎片化的流程拽到线上来。系统分为四个核心角色管理员维护基础数据收费员处理挂号和收费医生在门诊工作站里写病历开处方药房管理员负责药品入库、库存查询和发药。患者信息一旦建档后续每次就诊都挂在同一个ID下历史病历、过敏史、过往处方就能被医生直接调出来。说白了这就是一个轻量级的HIS只不过它的部署规模、功能边界和并发压力都是按中小门诊量设计的。1.2 双技术栈形态为什么会有Java和Django两个版本拿到这个标题的时候很多人第一反应是“怎么一个项目里混了SSM和Django到底用哪个”其实这里需要先理清一个问题这套资源交付的是同一套业务模型的两个技术实现版本而不是在一个项目里同时跑两套框架。JavaSSM版本是主推的生产形态。Spring、SpringMVC、MyBatis这套组合在国内的企业级项目里实在太成熟了招人好招、资料好查、遇坑也容易搜到解决方案。医院管理系统这种业务数据表和业务规则都很复杂MyBatis手写SQL的优势相当明显比如多表关联统计、按时间段分组统计营收写SQL比ORM硬拼要灵活得多。Django版本则是用Python快速实现的平行版本适合教学演示、学习者对照阅读或者后续想用Python做二次开发的情况。两套版本共享同一份数据库设计文档和业务流程文档所以你看论文文档的时候核心的业务分析和表结构设计是完全通用的。这种“一鱼两吃”的交付形态其实对学习者很友好。你可以先读文档理解业务再对照Java代码看SSM如何落地最后用Django版本验证自己对同一套业务模型的理解是否迁移得过去。能把一个业务模型用两套技术栈各自实现一遍比单纯抄一套代码的收获大得多。1.3 系统的角色边界与业务闭环任何管理系统第一步不是写代码而是把角色边界划清楚。这套系统里我按实际医院业务流程拆成了五个端管理员端维护科室、医生、药品目录、用户账号和权限是系统的“数据源头”收费员端挂号登记、收费结算、退号退费是整个门诊流程的入口和出口医生端查看候诊患者、写电子病历、开处方、开检查检验申请药房端药品入库、库存查询、处方发药以及效期和库存预警住院护士端部分版本中并入医生端入院登记、床位分配、医嘱执行、费用归集这五个角色串起来就是一条完整的业务闭环患者建档挂号 - 医生接诊写病历开处方 - 收费员收费 - 药房发药扣库存 - 如需住院入院登记 - 出院结算。任何一个环节脱节后面都会出问题。实际开发中我也是按照这条链路去设计数据库表关系的后面章节会详细展开。2. 技术选型与架构设计Java为主、Django为辅的落地逻辑2.1 SSM框架为什么是这类项目的“安全牌”先聊JavaSSM这套组合。Spring负责对象管理和事务控制SpringMVC负责HTTP请求的路由分发MyBatis负责数据库操作。这三层各管一摊边界很清晰。Spring的IoC容器可以理解为“后勤部”所有Service、Mapper这些对象的创建和依赖装配都交给它统一管理你只需要声明Autowired就能拿到对象不需要自己new。AOP则是“安检通道”日志记录、事务控制、权限校验这些横切逻辑可以在不侵入业务代码的前提下统一插入。比如每个Service方法里都要开事务、提交事务、异常回滚如果用AOP配上Transactional注解这些代码就完全不用写在业务方法里了。SpringMVC的工作模式我用一句话概括前端把请求发过来DispatcherServlet这个“前台接待”根据URL找到对应的Controller“业务员”业务员处理完返回数据接待再把响应交回给前端。这个模型理解透了整个Web开发的脉络就清晰了。MyBatis在这套系统里最大的价值是SQL可控。医院管理系统的报表统计特别多比如“统计本月各科室门诊量”、“查询库存低于阈值的药品”这些查询用MyBatis写XML映射文件非常顺手SQL优化也方便。相比之下如果所有查询都靠自动化ORM生成反而容易在复杂统计上绕弯路。2.2 Django版本适合什么场景Django版本并不是摆设。它的特点是“开箱即用”自带的ORM、Admin后台、认证系统、CSRF防护都是现成的。我用Django重写这套系统的时候大概只用了Java版本三分之一的时间因为Django的ORM把建表和增删改查的样板代码省掉了一大截。举个例子Django里执行查询和删除对象非常直观。查询用filter或get删除直接调delete()# 查询某个科室下所有在职医生 doctors Doctor.objects.filter(department_id1, status1) # 删除超过30天未登录的临时账号 TempUser.objects.filter(last_login__lttimezone.now() - timedelta(days30)).delete()这种风格的学习曲线比Java低不少。如果你是想快速理解“医院管理系统到底有哪些表和业务规则”Django版本配合自带的Admin后台点点鼠标就能看到所有数据表的数据体验非常直观。不过Django版本在应对复杂SQL统计时就需要绕一些弯子比如用annotate和aggregate来聚合统计遇到特别复杂的报表还是要写RawSQL。2.3 架构分层与请求流转过程两个技术栈的架构思路是一致的表现层、业务层、数据访问层三层分离。Java版本里对应Controller、Service、Mapper三个包Django版本里对应View、Service或直接用Model方法、Model三个层次。以Java版本为例一次“医生查询候诊患者列表”的请求流转是这样的患者列表页面AJAX请求 - SpringMVC的DoctorController接收参数 - 调用OutpatientService.getWaitingPatients(doctorId, date) - Service层做参数校验和业务判断 - 调用PatientMapper和RegisterMapper查询数据 - MyBatis执行SQL返回结果集 - Controller把结果封装成JSON返回前端这套链路里最容易写乱的是Service层。新手经常把业务逻辑堆在Controller里导致Controller几百行、Service空荡荡。我的习惯是Controller只做参数接收和结果返回所有“先判断再操作”的逻辑一律下沉到Service。比如挂号时先查号源余量再插入挂号记录这种操作必须在Service里加Transactional才能保证“扣号源记挂号”要么都成功要么都失败。3. 核心功能模块设计挂号、门诊、药房、住院一条线3.1 挂号与排班模块的设计逻辑挂号是整个系统的流量入口也是设计上最容易出问题的地方。排班数据是最上游的源头管理员先维护科室、医生、出诊时间段然后生成排班记录再为每个排班生成对应的号源池。我的做法是三个核心表排班表doctor_schedule、号源表register_source、挂号记录表outp_register。排班表存“哪个医生哪天在哪个科室出诊”号源表存“这个排班还有多少余号”挂号记录表存“哪个患者挂了哪个号”。为什么要把号源单独拆出来因为一个医生一上午可能放30个号每个号有独立的号码和状态待就诊、已就诊、过号、退号不拆表的话排班记录和挂号记录会形成一个一对多关联查询号源余量就变成统计表。挂号流程分两步走收费员在界面上输入患者身份证号系统自动判断是老患者还是新患者老患者直接带出历史档案新患者先建档再挂号。挂号时需要选择科室和医生系统展示剩余号数确认后生成挂号记录并扣减号源。这一步在Service层必须放在同一个事务里否则可能出现“挂号记录创建了但号源没扣减”的数据不一致。3.2 门诊医生工作站的业务流程医生端是使用频率最高的功能模块。医生登录后首先看到的是当前排班对应的候诊列表按挂号号码排序。点击接诊后系统记录接诊开始时间患者状态从“待就诊”变为“就诊中”。接诊界面核心是三块内容患者基本信息与历史就诊记录、电子病历录入区、处方开立区。电子病历这块我建议至少包含主诉、现病史、既往史、初步诊断这四个字段再预留一个检查检验申请区。诊断建议用ICD-10编码做下拉选择这样后续统计病种分布会非常方便但考虑到中小医院的实际情况也可以用文本输入加常用关键字联想。处方开立是医生端和药房端衔接的关键环节。医生在处方区选择药品、填写用量用法系统实时显示当前库存。处方保存后状态为“已开立”此时还不影响库存等到收费员完成收费后状态变为“已收费”药房才能看到这张处方并进行发药。这个状态机设计是整套系统能不能闭环的关键。3.3 药房库存管理与处方发药药品这块业务如果设计不好盘点的时候就会很痛苦。我的设计是药品目录表drug_catalog和药品库存表drug_stock分开目录表存药品通用名、规格、生产厂家、零售价库存表存某个批次的实际数量、批号、效期。入库操作很好理解药品入库时在库存表插入一条记录填写数量和效期。发药操作则需要配合处方状态变更药房看到“已收费”的处方后逐项核对药品和数量确认发药后扣减对应批次的库存。这里要注意库存扣减不能简单地在应用层先查库存再更新数量因为高并发时可能超卖。稳妥的做法是用一条带条件的UPDATE语句UPDATE drug_stock SET stock_num stock_num - #{num} WHERE drug_id #{drugId} AND stock_num #{num}这条SQL返回受影响行数如果为0说明库存不足本次发药直接失败回滚。这是库存类业务最常见的并发控制手法我在药店系统里也一直在用。另外效期管理不能省。很多中小医院药房过期药品问题频发系统里必须有“近效期药品列表”一般默认效期小于90天的药品在发药时给出黄色预警小于30天的禁止出库。这个规则看起来很细但正是这类细节决定了系统能不能真正用起来。3.4 住院管理与费用结算住院模块比门诊要复杂不少因为它是一个持续性的流程入院登记、分配床位、医生开长期/临时医嘱、护士执行医嘱、药品和检查费用逐项累加、最后出院统一结算。我把住院相关的核心表拆成住院登记表inp_hospital、住院费用明细表inp_fee_detail、床位表bed_info。入院登记时分配床位床位状态从“空闲”改为“占用”。住院期间所有费用包括药费、治疗费、检查费、床位费都通过费用明细表逐条记录每条记录关联住院号。这样出院结算时只需要按住院号汇总费用明细再减去预交金就能算出还需要补交多少或退还多少。这块设计时最容易被忽略的是“预交金”的概念。现实中住院都是先交押金后续费用不断累加如果系统没有预交金账户只有出院时一次性结算那每天的费用统计就会失真。我建议在住院登记表上增加deposit字段每次费用明细插入时同步更新住院表的total_fee字段这样护士站随时能看到“当前患者可用余额还剩多少”余额低于阈值时系统弹出催缴提醒这是非常贴近实际需求的细节。4. 数据库设计医疗数据的第一道生命线4.1 核心表结构与关系梳理数据库设计是整个项目的地基表关系设计错了后面所有代码都是白写。我以MySQL为例把这套系统的核心表梳理一遍。总表数大概在20张左右主流程表包括用户与权限、基础档案、门诊业务、住院业务、药品进销存这几大块。用户权限部分的核心设计是RBAC模型也就是用户-角色-权限三层表名作用关键字段sys_user系统用户表id, username, password, real_name, statussys_role角色表id, role_code, role_name, descriptionsys_user_role用户角色关联表user_id, role_idsys_permission权限表id, perm_code, perm_name, urlsys_role_permission角色权限关联表role_id, permission_id这套设计好就好在“用户不直接绑权限而是通过角色间接绑权限”。比如要给整个药房组增加一个报表导出权限只需要改药房角色和权限的关联不需要挨个改用户。基础档案部分核心是科室表base_department、医生表base_doctor、患者表base_patient。患者表要注意身份证号是常用检索字段必须加唯一索引。关联关系上医生和科室是多对一患者和挂号记录是一对多医生和排班是一对多。门诊业务部分挂号记录表outp_register和处方表diag_prescription是核心。挂号记录跟患者、排班、收费员都有外键关系处方表关联挂号记录同时处方明细表diag_prescription_item关联处方主表和药品目录表。一张处方对应多个药品明细这是典型的主子表结构。4.2 数据完整性与隐私约束医院系统的数据安全等级比普通业务系统高很多数据库层面至少要守住几条线。第一条是外键和索引。虽然很多开发为了性能会故意不建外键但医疗数据我更推荐在开发阶段保留外键至少保证“挂号记录的患者ID必须存在于患者表”否则一旦代码里出现脏数据后期排查会非常痛苦。当然上线后如果发现外键影响了大表插入性能可以再评估去掉外键、改用应用层校验。每张表的主键、唯一索引、常用查询字段索引在开发文档里都要写清楚比如挂号记录表必须按“挂号日期医生ID”建联合索引因为医生端候诊列表就是按这个条件查的。第二条是金额字段一律用DECIMAL不用FLOAT和DOUBLE。药品价格、费用结算这种涉及钱的数据用浮点类型会出现0.10.2不等于0.3这种经典问题。DECIMAL(10,2)完全可以覆盖中小医院的金额范围。第三条是隐私字段不能明文存储。患者手机号、身份证号在列表展示时要脱敏密码必须用BCrypt等加盐哈希算法存储绝不能明文入库。数据库连接的用户名密码也不要硬编码在代码里至少放到独立的配置文件中。虽然中小医院系统的数据量不大但从一开始就养成安全习惯后面做任何企业级项目都用得上。5. 核心功能实现细节从登录鉴权到处方发药5.1 登录鉴权与角色权限控制Java版本的权限控制我用的是拦截器加自定义注解的方式没有引入太重的安全框架核心思路是“登录校验 角色编码校验”两层。登录成功后把用户ID和角色编码列表放进Session。定义AuthInterceptor拦截器在preHandle里校验Session是否存在不存在就重定向到登录页。对于需要特定角色才能访问的接口定义一个RequireRole注解在拦截器里读取注解上的角色编码跟当前用户的角色列表比对不匹配就返回403。这个方案轻量、易读非常适合学习。Django版本则可以用内置的django.contrib.auth和配套的装饰器比如login_required和自定义的user_passes_test来做角色校验代码会更简洁一些from django.contrib.auth.decorators import login_required, user_passes_test def is_pharmacist(user): return user.roles.filter(codePHARMACIST).exists() login_required user_passes_test(is_pharmacist) def drug_issue(request, presc_id): # 药房确认发药逻辑 pass密码安全方面Java版推荐使用jBCrypt或者Spring Security的BCryptPasswordEncoderDjango内置的make_password和check_password已经默认使用PBKDF2加盐哈希直接用就行。5.2 挂号与号源扣减的实现要点挂号这个操作在代码层面要处理的细节非常多这里我给大家看一个Java版本的挂号方法骨架重点看注解和事务边界Transactional(rollbackFor Exception.class) public RegisterResult register(RegisterRequest req) { // 1. 校验患者是否存在不存在则创建 Patient patient patientMapper.selectByIdCard(req.getIdCard()); if (patient null) { patient createPatient(req.getPatientInfo()); } // 2. 查询当前排班对应的号源加行锁防止并发超挂 RegisterSource source registerSourceMapper.selectByScheduleIdForUpdate(req.getScheduleId()); if (source.getRemainNum() 0) { throw new BizException(该医生当前号源已挂满); } // 3. 生成挂号记录状态为待就诊 Register register new Register(); register.setPatientId(patient.getId()); register.setScheduleId(req.getScheduleId()); register.setStatus(WAIT); register.setFee(source.getRegFee()); registerMapper.insert(register); // 4. 扣减号源 source.setRemainNum(source.getRemainNum() - 1); registerSourceMapper.updateById(source); return new RegisterResult(register.getId(), source.getRemainNum()); }这里有几个细节需要特别说明。selectByScheduleIdForUpdate是用了SELECT ... FOR UPDATE的行级锁锁住这一条号源记录确保同一个号源不会被两个并发请求同时扣减。第1步和第4步都在同一个事务里任何一个操作抛异常整个挂号记录和号源扣减都会回滚不会出现“号挂了库存没扣”的情况。实际测试中我用JMeter模拟50个并发请求同时挂同一个医生的号号源余数为30最终成功入库的挂号记录恰好30条这个场景直接验证了事务和锁的正确性。5.3 处方与药品库存联动处方状态机是整套系统里最容易理解错的部分。我在设计时把一张处方拆成四个状态已开立、已收费、已发药、已作废。每个状态由哪个角色触发是有严格边界的状态触发角色前提条件后续操作已开立医生处方保存成功病历与处方关联已收费收费员费用已结清处方对药房可见已发药药师库存充足扣减库存记录发药人已作废医生/管理员患者在收费前取消原处方标记作废状态流转代码用枚举加状态校验来保证合法性。比如药房发药时如果处方状态不是“已收费”直接抛出异常拒绝发药。这一步校验看着简单但能挡住大量误操作比如医生刚开完处方还没收费药房就把药发了这种情况在实际使用中确实出现过。药品库存扣减的并发问题在前面已经提过用条件UPDATE来解决。这里再补充一点当处方包含多个药品时整个发药流程仍然要在一个事务里只要有一个药品库存不足所有药品都不能出库否则会出现“患者只拿到了部分药”的尴尬局面。我在调试文档中特别标记了这个场景也建议在药房界面加一个“发药失败时整单回滚”的提示文案。5.4 费用结算与报表统计费用结算分两块门诊收费在挂号或处方开立后即时完成住院费用则持续记账、出院时统一结算。门诊收费的代码逻辑我建议做成“费用单模式”患者可能同时有挂号费、诊疗费、药费一次性合并成一张收费单而不是每个项目单独收款。收费单表bill加明细表bill_item明细表关联对应的业务单据比如挂号记录ID或处方ID。这样对账时只需按时间查收费单不用满库找零散的费用记录。报表统计这块SSM版本我主要用MyBatis写聚合SQL。比如统计当月每天的门诊收入select idsumDailyClinicIncome resultTypemap SELECT DATE(bill.create_time) AS day, SUM(bill.total_amount) AS amount FROM bill WHERE bill.create_time BETWEEN #{startDate} AND #{endDate} AND bill.status PAID GROUP BY DATE(bill.create_time) ORDER BY day /selectDjango版本的等价实现可以用ORM的annotate函数from django.db.models import Sum, Count from django.db.models.functions import TruncDate daily_stats Bill.objects.filter( create_time__date__gtestart_date, create_time__date__lteend_date, statusPAID ).annotate( dayTruncDate(create_time) ).values(day).annotate( total_amountSum(total_amount), bill_countCount(id) ).order_by(day)报表的查询频率很高建议在对应的日期字段上加索引避免后续数据量上来后统计接口越跑越慢。虽然中小医院的日门诊量也就是几百单但养成设计索引的习惯没有坏处。6. 常见问题与排查技巧实录6.1 Java版本编译与部署阶段的典型报错这个项目的整套调试文档里记录最多的就是环境问题。我挑几个高频问题分享给各位。Maven依赖冲突是SSM项目的老大难。典型症状是启动Tomcat时报NoClassDefFoundError或者BeanCreationException原因通常是不同依赖传递进来了相同类的不同版本。排查思路很固定用mvn dependency:tree查看依赖树找到冲突的依赖在pom.xml里用exclusion排除不需要的版本。比如常见的jackson-databind和spring-boot-starter-json版本冲突我的做法是统一用dependencyManagement锁定版本号。端口占用问题同样高频。Tomcat默认8080但机器上经常有别的服务占用报错是 Port 8080 was already in use。启动前用netstat -ano | findstr 8080查一下谁占着端口或者干脆把conf/server.xml里的端口改成8081。这个问题虽然简单但几乎每次给新手调试都会遇到。JDK版本不匹配也很隐蔽。SSM项目大多基于JDK 8开发如果你机器上装的是JDK 17编译时直接报错或者运行时不兼容。我现在的习惯是给电脑装一个JDK 8和一个JDK 11用环境变量和IDE的Project Structure切换遇到老项目默认切回JDK 8基本能避开一半的编译问题。6.2 数据库中文乱码与事务回滚异常MySQL中文乱码是SSM项目的“千古难题”而且经常是“数据本身乱、代码看着没问题”。排查链条必须完整走一遍MySQL服务端字符集、数据库字符集、表字符集、JDBC连接URL、前端页面编码任何一处不是utf8mb4都可能乱码。JDBC连接URL里记得加上characterEncodingutf8mb4和serverTimezoneAsia/Shanghai前者管中文后者管时间戳不偏移不设置serverTimezone在MySQL 8下会直接报Server returns invalid timezone错误。事务不回滚也是个很容易踩的坑。Transactional注解默认只回滚RuntimeException和Error如果你在Service方法里手动catch了Exception并吞掉事务就不会感知到异常数据照样提交。正确做法是要么不在事务方法内捕获异常要么在catch块里手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。另一个常见问题是事务方法必须是public并且不能是同类内部方法自调用否则Spring的AOP代理不会生效事务静默失效这个坑特别隐蔽代码看起来没问题但就是不回滚。6.3 Django版本的跨域、静态文件与调试常见坑Django版本虽然开发快但坑也不少。最常见的是跨域问题。如果用前后端分离模式前端页面跑在5500端口Django跑在8000端口直接发AJAX请求会被浏览器拦下来。解决方案是安装django-cors-headers然后在settings.py里配置INSTALLED_APPS [ # ... corsheaders, ] MIDDLEWARE [ # ... corsheaders.middleware.CorsMiddleware, ] CORS_ALLOWED_ORIGINS [ http://localhost:5500, ]静态文件404的问题主要集中在生产模式。DEBUGFalse后Django默认不再托管静态文件页面里的CSS和JS全部丢失。解决思路有两种开发测试阶段用WhiteNoise或者django.contrib.staticfiles的runserver方式临时托管正式部署就交给Nginx处理。我调试文档里明确建议初学者先不要关闭DEBUG除非你已经把所有静态文件收集到了STATIC_ROOT目录。还有一个容易忽略但很影响体验的是CSRF问题。如果前端页面用jQuery的$.ajax向后端POST数据Django默认会校验CSRF Token没带上就返回403。Django官方文档的解决方案是在模板里用{% csrf_token %}获取Token然后通过请求头带上$.ajaxSetup({ headers: { X-CSRFToken: getCookie(csrftoken) } });这个问题在Django版本中几乎人人都会遇到提前写进调试文档能帮使用者省下半小时排查时间。一点收尾的个人体会做完这个项目最大的体会是管理系统的技术难度其实不在框架本身而在于业务流程梳理得是否透彻数据模型设计得是否经得起推敲。拿医院这种业务来说一张处方从医生开立到药房发药要经历状态流转一次挂号要同时保证号码和号源一致这些规则如果没想清楚就动手写代码后面返工是必然的。我个人的习惯是拿到任何管理系统需求先画一张业务流程图把角色、动作、状态、数据流向都标出来再开始设计表结构和接口整个过程反而比急着堆代码快得多。还有一个实用小技巧想分享给正在做毕设的同学们源码、论文文档、调试文档三件套一定要同步维护不要写完代码再补文档。我在这个项目里每写完一个模块就顺手把关键表结构、接口说明和踩坑记录补进文档最后整理成文的时候基本只是排版而不是对着代码回忆。等你答辩或者写完项目总结的时候会感谢当初这个习惯的。
返回列表