ARTICLE DETAIL

资讯详情

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

Java门诊服务聚合系统:挂号到收费全流程源码解析

Java门诊服务聚合系统:挂号到收费全流程源码解析 简介基于Java语言开发的门诊服务聚合系统设计源码面向医疗信息化开发者与Java后端学习者以聚合门诊服务为核心覆盖预约挂号、排队叫号、医疗记录管理等典型业务模块。压缩包内共51个文件包含34个Java源文件、8个XML配置文件、1个YAML配置及1个JAR包等配套Maven构建脚本与项目说明文档整体大小为220KB。项目采用Spring Boot、Spring MVC与MyBatis技术栈遵循模块化、服务化设计前后端分离重点展示医疗场景下接口定义、数据持久化及安全认证的实现方式。已有246人学习下载适合需要借鉴完整门诊系统源码结构、了解Java企业级医疗项目配置与部署细节的读者。通过阅读源码可掌握多模块Maven工程的组织方式、YAML多环境配置及关键业务逻辑的代码组织为同类系统设计提供直接参考。1. 门诊服务聚合系统一套能跑通挂号到诊疗流程的 Java 课程设计源码医院门诊楼里最典型的场景患者先在窗口排队挂号再拿着挂号单去分诊台排队然后被叫号看医生医生开完检查单或处方患者又要去收费窗口排队最后再跑回诊室听结果。这个过程中患者的身份信息被反复录入不同环节的系统互不相通医生想看一眼患者三个月前的就诊记录都费劲收费处也不清楚当前号源到底锁到了几号。基于 Java 语言的门诊服务聚合系统设计源码要解决的正是这个痛点——把挂号、分诊、接诊、收费这些原本分散的服务聚合到一个统一入口让患者一次登录就能走完整条诊疗链路也让医生和收费员在各自的工作台上看到同一份患者数据。这份源码适合两类人。一类是做 Java 课程设计或毕业设计的学生需要一套结构清楚、能讲明白业务逻辑的完整工程而不是零散的功能片段另一类是刚接触医疗业务开发的从业者想看看门诊场景下挂号、分诊、收费这些模块的状态流转到底怎么落代码。它不是一个只停留在增删改查的 Demo而是把门诊主流程拆成了可扩展的服务模块前后端能连起来跑。下面从业务建模、源码结构、核心模块到部署排错把这份资源从头到尾拆开讲。2. 业务建模与聚合设计先看清门诊怎么跑再谈代码怎么分2.1 门诊主流程拆解挂号-分诊-接诊-收费的链路设计门诊系统的业务核心是一串有先后顺序的状态流转不是孤立的功能点。患者先到挂号窗口选择科室和医生系统锁定号源并生成挂号记录此时状态是“已挂号”随后患者到分诊台报到护士确认候诊状态变为“候诊中”医生叫号接诊后状态更新为“就诊中”医生录入主诉、诊断并开出处方或检查单患者去收费处缴费状态变成“已缴费”整个闭环才算完成。这条链路里最容易出错的是状态边界。比如患者挂了号但没去分诊号源算不算释放医生接诊时患者还没缴费检查单能不能先开这份源码的处理方式是给每个环节都维护一个明确的状态字段用常量去定义而不是靠字符串散落在业务代码里。聚合设计的核心也在这里——不是让每个模块各自维护一份患者数据而是通过一个聚合服务层把底层多个基础服务串联起来对外暴露统一接口。从代码层的角度说这份源码用的是典型的分层结构Controller 只做参数接收和结果封装Service 层处理业务规则Mapper 层负责数据库交互。聚合服务层放在 Service 之上比如OutpatientAggregateService它内部调用挂号、分诊、接诊、收费四个基础 Service组合出一个完整的门诊流程接口。这样前端只需要调用一个聚合接口后端各模块的职责边界却能保持清晰后续要加检查单模块或者加药房发药模块改动被限制在聚合层内部不至于牵一发动全身。2.2 为什么选“聚合服务”而不是单表 CRUD门面模式的取舍很多课程设计做医疗系统写着写着就变成了一张大表的增删改查——一个registration表里既存患者信息又存医生信息还存缴费状态前端页面直连数据库查询。这种写法开发速度快但业务规则一变就崩比如要限制一个医生上午最多接诊 30 个患者这个校验逻辑应该放在哪里如果直接写在 JSP 页面里那换一个页面就要重写一遍。这份源码走的是门面模式Facade Pattern的路线把散落在各模块的业务逻辑统一收口到聚合服务层。前端拿到的返回结构始终是统一的ResultT对象包含状态码、提示信息和具体数据。比如挂号成功后返回{ code: 200, msg: 挂号成功, data: { registrationId: 1024, queueNumber: 15 } }前端不用关心这个结果到底是挂号模块生成的还是排队模块生成的只管解析数据结构。选这个方案的好处集中体现在两个场景。第一是事务边界变清晰了一次完整的“挂号锁定号源生成排队序号”是一个事务这个事务以聚合服务的方法为边界而不是散在多个 Controller 里各管一段第二是扩展性有了明确方向门诊系统后续增加体检预约、报告查询只需要在聚合层加新方法原有接口不动。缺点也有——聚合层代码会膨胀所以这份源码里聚合 Service 内部做了分包按业务域拆成registration、triage、consultation、charge四个子包避免所有逻辑都堆在一个类里。2.3 数据模型落库患者、医生、号源、处方四大核心表结构打开源码里的数据库脚本先看到的是建库建表语句。整个库按业务域拆了十来张表但真正决定门诊主流程跑通的是四张核心表患者表、医生表、挂号表、处方表。先看患者表和医生表的设计逻辑。CREATE TABLE patient ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 患者主键, card_no VARCHAR(32) NOT NULL UNIQUE COMMENT 就诊卡号, name VARCHAR(32) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL COMMENT 性别0-女 1-男, phone VARCHAR(16) DEFAULT NULL COMMENT 手机号, birthday DATE DEFAULT NULL COMMENT 出生日期, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_card_no (card_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT患者表;患者表的关键是card_no加了唯一索引这是患者的身份凭证后续挂号、分诊、缴费都以它为关联条件。身份证号、手机号都只是附属信息真正驱动业务流转的是这张就诊卡号。医生表则更侧重科室归属和排班状态一个医生属于一个科室每天可以有多条排班记录每条排班记录对应一个时段和号源总数。CREATE TABLE registration ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 挂号主键, card_no VARCHAR(32) NOT NULL COMMENT 就诊卡号, doctor_id INT NOT NULL COMMENT 医生ID, schedule_id INT NOT NULL COMMENT 排班ID, registration_type TINYINT NOT NULL COMMENT 号别0-普通 1-专家, fee DECIMAL(10,2) NOT NULL COMMENT 挂号费, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0-已挂号 1-候诊中 2-就诊中 3-已缴费 4-已退号, queue_number INT NOT NULL COMMENT 排队序号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_card_no (card_no), KEY idx_doctor_id (doctor_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT挂号记录表;挂号表是这个系统的状态中枢。status字段从 0 到 4 串起了整条门诊链路queue_number是医生叫号的依据schedule_id关联到排班表目的是防止医生不在岗时患者还能挂上号。处方表则记录医生开出的药品明细通过registration_id关联回挂号记录收费模块直接读处方表算总价。3. 源码结构与登录鉴权从建库脚本到 Shiro 会话控制3.1 源码包目录在讲什么后端分层与前端页面一一对应把源码拉下来后先看目录结构再谈跑起来。整个工程是标准的 Maven 多模块布局但实际业务代码集中在src/main/java下的几个包里面。controller包负责接收前端请求service包放业务逻辑mapper包放 MyBatis 的接口和 XML 映射文件entity包对应数据库表结构config包放 Shiro、拦截器和全局配置。前端页面在src/main/webapp/WEB-INF/jsp下按patient、doctor、admin、charge四个角色目录划分。一个值得注意的细节是config包里有一个GlobalExceptionHandler用ControllerAdvice注解统一捕获业务异常。这意味着前端拿到的错误信息格式是固定的不会出现某个接口直接抛出一长串堆栈异常的情况。对于门诊这类对数据准确性要求高的业务场景统一异常处理不是加分项而是必需品。3.2 建库脚本跑通数据库初始化与核心数据准备数据库初始化是大部分人第一次翻车的地方。源码里自带sql/init.sql里面不只是建表语句还插入了基础数据管理员账号、两个科室、三个医生、几条排班记录和测试患者。执行时建议用 MySQL 5.7 及以上版本字符集选择utf8mb4排序规则选utf8mb4_general_ci。mysql -uroot -p123456 sql/init.sql执行成功后用SHOW TABLES;验证是否生成了全部表。常见的问题是初始化脚本执行时 MySQL 的sql_mode限制了TIMESTAMP默认值如果报Invalid default value for create_time需要在 MySQL 配置里加上sql_modeNO_ENGINE_SUBSTITUTION或者执行SET sql_mode;后再跑脚本。这一步完成后最重要的是确认测试患者数据已经存在后续登录和挂号都要靠这批初始数据。3.3 登录与权限控制Shiro 过滤器链的配置要点登录鉴权用的是 Apache Shiro配置集中在config/ShiroConfig.java里。这套源码没有引入 Spring Security原因是 Shiro 的过滤器链更直观对于课程设计阶段的权限控制足够用。核心配置是这段Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); // 登录接口和静态资源匿名访问业务接口全部需要认证 MapString, String filterChainDefinitionMap new LinkedHashMap(); filterChainDefinitionMap.put(/login, anon); filterChainDefinitionMap.put(/logout, logout); filterChainDefinitionMap.put(/css/**, anon); filterChainDefinitionMap.put(/js/**, anon); filterChainDefinitionMap.put(/images/**, anon); // 权限控制/doctor/** 需要医生角色/admin/** 需要管理员角色 filterChainDefinitionMap.put(/doctor/**, roles[doctor]); filterChainDefinitionMap.put(/charge/**, roles[charge]); filterChainDefinitionMap.put(/admin/**, roles[admin]); filterChainDefinitionMap.put(/**, authc); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); factoryBean.setLoginUrl(/login); factoryBean.setUnauthorizedUrl(/403); return factoryBean; }这段配置的逻辑是按 URL 前缀做角色隔离医生只能访问/doctor/**下的页面收费员只能访问/charge/**管理员拥有全部权限。有一个参数需要特别关注filterChainDefinitionMap用的是LinkedHashMap保证匹配顺序是“先声明先匹配”所以/login必须放在最前面否则会被/**的authc规则拦截导致登录页都进不去。4. 核心模块实现拆解挂号分诊与就诊记录的代码复现4.1 挂号模块号源锁定与排队队列的状态流转源码里挂号模块的入口是RegistrationController它接收科室 ID、医生 ID、就诊卡号三个参数然后调用聚合服务层的createRegistration方法。这个方法的实现是整个挂号模块的核心它的逻辑顺序很有讲究先校验医生当天是否有排班再校验号源是否已满然后锁定号源、生成排队序号、创建挂号记录最后返回结果。Service public class OutpatientAggregateService { private final RegistrationService registrationService; private final ScheduleService scheduleService; private final PatientService patientService; public RegistrationResult createRegistration(String cardNo, Integer doctorId, Integer scheduleId) { // 1. 校验患者是否存在且状态正常 Patient patient patientService.getPatientByCardNo(cardNo); if (patient null) { throw new BusinessException(就诊卡号不存在请先建档); } // 2. 校验排班当天是否有效同时锁住排班记录 Schedule schedule scheduleService.lockSchedule(scheduleId); if (schedule null || schedule.getSurplus() 0) { throw new BusinessException(该医生当前时段号源已满); } // 3. 计算排队序号当前序号 1 int nextQueue schedule.getCurrentQueue() 1; // 4. 创建挂号记录状态为已挂号 Registration registration registrationService.create( cardNo, doctorId, scheduleId, nextQueue); // 5. 扣减剩余号源返回聚合结果 scheduleService.deductSurplus(scheduleId); return RegistrationResult.success(registration); } }这里有两个容易被忽略的细节。第一个是scheduleService.lockSchedule方法它不是简单的查表而是用SELECT ... FOR UPDATE把排班记录锁住目的是防止两个患者同时挂号时读到同一个剩余号源数——这就涉及到并发场景下的数据一致性后面排查章节会展开讲。第二个是排队序号currentQueue 1的生成方式依赖数据库中的当前值如果挂号记录创建失败这个序号会回滚不会出现跳号或者重号。4.2 分诊与接诊医生工作台的忙闲状态控制分诊模块在源码里的设计比较轻原因在于它的核心逻辑其实是状态更新。护士在分诊台扫描患者的就诊卡号系统把挂号记录的状态从“已挂号”0改成“候诊中”1然后医生端的工作台刷新列表时只查询状态为 1 的记录。Transactional public void triage(String cardNo, Integer registrationId) { // 只有状态为已挂号的记录才能被分诊防止重复报到 Registration registration registrationMapper .findByIdForUpdate(registrationId); if (registration null || registration.getStatus() ! 0) { throw new BusinessException(该患者当前状态不可分诊); } registration.setStatus((byte) 1); // 候诊中 registrationMapper.updateStatus(registration); }接诊模块的代码逻辑类似但当医生点击“开始接诊”时源码里还额外更新了医生表的状态字段把医生的值班状态从“空闲”改成“忙碌”。这意味着源码里实际上维护了两份状态患者的就诊状态和医生的忙闲状态。两份状态必须同时更新因为后续患者的就诊记录会关联到医生当前状态排班模块也要靠医生状态判断能否继续接诊。4.3 收费与退费事务边界和状态回滚处理收费模块是整条链路里最强调事务边界的地方。患者拿着处方去收费窗口收费员输入就诊卡号系统调出待缴费的处方明细确认金额后调用收费接口。这个接口内部是一个跨两个表的操作更新挂号记录状态为“已缴费”同时生成一条收费流水记录。如果只更新了挂号状态而收费流水写入失败患者会看到“已缴费”但财务对不上账。Transactional(rollbackFor Exception.class) public ChargeResult charge(String registrationId, BigDecimal paidAmount) { // 查询挂号记录校验当前状态必须为就诊中医生已写完病历 Registration registration registrationMapper .findByIdForUpdate(registrationId); if (registration.getStatus() ! 2) { throw new BusinessException(该患者尚未完成就诊无法缴费); } // 计算处方总金额与支付金额对比 BigDecimal total prescriptionMapper .sumAmountByRegistrationId(registrationId); if (total.compareTo(paidAmount) ! 0) { throw new BusinessException(缴费金额与处方金额不一致); } // 更新挂号状态 写入流水同一个事务 registrationMapper.updateStatus(registrationId, (byte) 3); ChargeRecord record new ChargeRecord(); record.setRegistrationId(registrationId); record.setAmount(total); record.setChargeTime(new Date()); chargeMapper.insert(record); return ChargeResult.success(total); }退费逻辑的边界条件更多。源码里规定退费只能在当天进行且挂号记录状态必须是“已缴费”退费操作会同时把状态回退为“已就诊”并把流水标记为作废而不是物理删除。这个设计是合理的——财务数据不允许删除只能冲销或者作废否则对账时会出现“钱收了但流水凭空消失”的问题。Transactional(rollbackFor Exception.class)这一段值得单独拿出来说rollbackFor指定了任何异常都触发回滚如果不写这个参数Spring 默认只在运行时异常时回滚受检异常不会回滚很容易造成半个事务提交的悬空状态。5. 门诊系统常见问题排查部署、乱码与患者数据不一致5.1 启动即报 Bean 创建异常大概率是 MyBatis 映射扫描路径没对上现象Spring Boot 启动时抛Invalid mapper interface或BeanCreationException指向某个 Mapper 接口无法注入。原因源码里的MapperScan扫描路径写的是com.hospital.outpatient.mapper但工程里实际的 Mapper 接口所在包名和它不一致或者某个 Mapper XML 的namespace与接口全限定名对不上。解决定位到Application.java或配置类上的MapperScan注解改成实际包路径同时检查resources/mapper目录下 XML 文件的namespace是否等于接口全类名改完清理一次target目录再重启。5.2 页面能开但登录失败数据库编码与字符集不一致现象前端页面正常加载输入管理员账号密码后提示登录失败但数据库里明明有这条用户记录。原因初始化 SQL 脚本执行时用了默认字符集而 MySQL 的系统变量character_set_server是latin1导致中文用户名写入后乱码查询时匹配不上。解决查一下SHOW VARIABLES LIKE character%;如果字符集不对把建库语句改为CREATE DATABASE hospital DEFAULT CHARACTER SET utf8mb4;重新导入init.sql。源码里的登录逻辑是先用用户名查用户再比对密码哈希值所以用户名乱码会直接导致“查不到用户”。5.3 挂号重复插入并发场景下号源校验与事务隔离级别现象用两个浏览器窗口同时给同一个医生挂号看到剩余号源都显示还剩 1 个结果两个窗口都挂号成功号源变成负数。原因挂号接口不是原子的先查schedule.surplus再扣减两个并发事务都读到了剩余 1随后各自执行扣减没有锁保护。解决在我自己的复现中把scheduleService.lockSchedule里的查询改成SELECT ... FOR UPDATE只锁排班记录行同一个医生同一时段的并发挂号就会被串行化同时把OutpatientAggregateService的createRegistration方法加上Transactional保证扣减号源和创建挂号记录在同一个事务里。另外注意 MySQL 的默认隔离级别是REPEATABLE_READ加了行锁后不会有幻读问题不需要改隔离级别。5.4 前端页面接口 404Shiro 过滤器把静态资源拦截了现象登录后能进入主页框架但页面上的 CSS 样式全部丢失点击菜单时部分接口返回 404。原因Shiro 的过滤器链把/css/**、/js/**、/images/**之外的其他静态资源路径拦截了比如 Bootstrap 字体文件走了/fonts/**而这条规则没有放行同时 JSP 页面里的资源引用路径写的是绝对路径/js/jquery.min.js但工程部署的上下文路径带项目名路径匹配不上。解决在ShiroConfig的过滤器链里补上/fonts/**、/favicon.ico的放行规则JSP 页面里统一用c:url或者${pageContext.request.contextPath}拼接资源路径避免上下文路径不一致导致的 404。6. 从课设到生产门诊系统的二次开发与面试话术6.1 给系统加一个排队叫号大屏的扩展思路跑通主流程之后最值得做的二次开发是加一个排队叫号展示大屏。这个功能在生产环境几乎必配但源码里没有直接实现需要自己扩展。扩展思路是在聚合服务层加一个getCurrentQueue(doctorId)接口返回当前医生正在接诊的排队序号和接下来三个等待序号的列表前端大屏页面每五秒轮询一次这个接口把queueNumber实时刷新到页面上。// 大屏轮询逻辑每5秒拉取一次当前队列 function refreshQueue(doctorId) { $.get(/queue/current?doctorId doctorId, function (res) { if (res.code 200) { $(#currentNumber).text(res.data.currentQueueNumber); $(#waitList).empty(); res.data.waitList.forEach(function (item) { $(#waitList).append( li请 item.queueNumber 号 item.patientName 到 item.roomName 就诊/li ); }); } }); } setInterval(function () { refreshQueue(1); }, 5000);这一步改动能让你在答辩或项目汇报时讲出“实时数据刷新”“轮询与并发读”两个亮点。如果时间充裕还可以把轮询改成 WebSocket 推送作为加分项写进文档。6.2 简历与面试这门诊系统源码该怎么讲才算自己写的医疗门诊系统在 Java 面试里属于高频的业务场景题很多公司招聘时不看你会不会 Spring Boot CRUD而是看你能否讲清楚业务状态流转和并发场景下的数据一致性。这份源码如果只是下载跑通面试时三句话就被问穿。建议按这个顺序组织话术先讲业务说明门诊主流程的五个状态和每个状态流转的触发条件再讲架构说明为什么不直接改数据库表而是通过聚合服务层最后讲技术难点抛出并发挂号、事务回滚和 Shiro 过滤器链这三个点每个点都用自己的话解释清楚场景和解决方案。比如面试官问“你这个系统怎么防止号源超卖”标准回答不是背代码而是说我先用SELECT ... FOR UPDATE锁住排班记录再判断剩余号源然后在一个事务里完成扣减和挂号记录插入。这句话能同时体现你对并发控制和事务边界的理解比“用的 MyBatis 写的”好太多。还有一点面试时不要说“这个项目是下载的源码”要说“我在 xx 源码基础上做了二次开发主要重构了聚合服务层和并发控制逻辑”这是技术圈默认的诚实表达。6.3 收尾我自己跑这套源码时的两个习惯我每次拿到一套新的课设级源码第一件事一定不是急着启动而是先打开init.sql看表结构把外键关系在纸上画一遍。这套门诊系统的表结构不算复杂但挂号表和排班表之间的关联、处方表和挂号表之间的关联如果不先看清楚后面排查并发问题时会很被动。第二个习惯是启动后先不开浏览器用 Postman 直接调接口测业务流从建卡到挂号到分诊到缴费一路调通然后再去点页面。这样能把后端逻辑问题和前端页面问题分开排查不至于混在一起翻车。希望这套门诊服务聚合系统源码的拆解能帮到你至少让你拿到代码后少走几段弯路。本文还有配套的精品资源点击获取
返回列表