最近又有不少人问我SpringBoot医院系统到底该怎么做,尤其是这个标题特别典型——"springboot基于Java Web的医院就诊系统医生排班预约挂号电子病历药品(源码+文档+运行视频+讲解视频)",几乎就是当下Java Web方向毕业设计、课程设计的"标准答案式命题"。这套系统表面上看是个常规CRUD项目,但真正动手做进去就会发现,它的业务闭环非常完整:患者建档、预约挂号、医生排班、电子病历、药品管理、费用结算,每一条线都互相咬合,做的时候稍不留神就会在一堆外键关系里绕晕。
先给结论,这套系统适合三类人:一是正在选毕业设计题目、需要一份能跑通答辩全流程方案的在校生;二是刚学完SpringBoot基础、想通过一个完整医疗场景把框架、数据库、前后端串起来的Java学习者;三是小诊所、校医院等需要快速搭建就诊流程原型的开发者。无论你是哪一类,这篇文章都会把项目的模块拆解、数据库设计、关键逻辑实现、部署排坑全部摆出来讲透,你看完可以直接照着复现。
1. 项目定位与技术选型思路
1.1 这套医院就诊系统到底是什么
先说业务全景。整套系统围绕"就诊"这件事划分出三个端:
- 患者端:注册登录、选择科室与医生、查看排班、预约挂号、查看电子病历、在线缴费(或到院缴费)。
- 医生端:查看今日排班、接诊患者、书写电子病历、开立处方(关联药品)、检查患者历史病历。
- 管理端:维护医生信息、设置排班规则、管理药品目录、审核挂号记录、查看科室接诊统计。
这三个端看是独立功能,实际共享同一套数据底座。比如患者挂号的本质,是去"消费"医生排班生成的号源;医生写病历的核心,是给一次"已就诊"的挂号记录补充诊断信息;开药的本质,是在病历上下文里创建处方明细,同时触发药品库存扣减。整个项目做完,你对"业务闭环"这个词的理解会比看十篇教程都深。
从工程结构看,这套系统有两种常见形态。一种是经典单体架构:SpringBoot负责接口与业务,前端用Thymeleaf或JSP渲染页面,数据层用MyBatis Plus操作MySQL。另一种是前后端分离:前端用Vue或Layui,后端提供JSON接口。标题里提到的源码、文档、运行视频、讲解视频,基本都围绕单体版本在组织,因为单体版对初学者最友好,部署简单、调试直观、一个jar包就能跑起来,非常适合作为毕业设计交付物。
1.2 技术栈为什么这样选
很多学员会纠结:为什么不是SSH?为什么不是SSM?为什么不是SpringBoot配JPA?我把选型理由拆开说:
| 技术组件 | 本项目方案 | 选型理由 |
|---|---|---|
| 基础框架 | SpringBoot 2.x | 内嵌Tomcat,免去复杂XML配置,启动即用,是目前Java初学者的主流起点 |
| ORM层 | MyBatis Plus | 单表CRUD不需要写SQL,分页查询、逻辑删除开箱即用,比原生MyBatis少写60%模板代码 |
| 数据库 | MySQL 5.7+ | 免费稳定,教务环境最熟悉,事务支持完善,适合业务型系统 |
| 缓存(可选) | Redis | 用于挂号锁号、验证码存储、热点科室缓存,不加也能跑,加了面试答辩加分 |
| 前端模板 | Thymeleaf / Vue | 单体版用Thymeleaf,前后端分离版用Vue+Layui,看进度选择 |
选SpringBoot而不是SpringMVC单体项目,核心原因只有一个字:快。SpringBoot的自动装配机制把视图解析、字符编码、JSON序列化这些琐事全部托管,你只要关注业务代码。比如你想确认项目里用了哪些依赖,直接看pom.xml;想调整端口,改application.yml的server.port即可。对答辩来说,"用SpringBoot简化了传统SSM的配置流程,提升了开发效率"这句话本身就是很好的项目亮点。
这里补充一个认知:理解了SpringBoot自动装配原理,排错能力会高一个档次。比如你发现配置了数据源但启动报"Failed to configure a DataSource",其实是SpringBoot根据classpath自动判断要用的数据源类型,找不到才报错。这个问题我在第4章会重点讲。
2. 核心业务模块与数据库设计
2.1 医生排班机制怎么建模
排班是整个系统的"号源发动机",挂号、统计、病历全都依赖它。设计排班表时,我建议你按照"医生-日期-时段-号源"四个维度去建模型,不要简单存一个字符串了事。
排班表(schedule)核心字段参考:
schedule_id -- 主键 doctor_id -- 关联doctor表 dept_id -- 关联科室表,冗余存储便于按科室筛选 schedule_date -- 排班日期 period -- 时段:1上午 2下午 3晚 total_number -- 总号源数 remain_number -- 剩余可约号数 status -- 排班状态:0停诊 1正常为什么需要冗余dept_id?因为实际查询场景里,患者永远先选科室、再看医生,如果每次都要通过doctor_id反查科室,SQL多一次join,页面响应会变慢。做医排班生成时,管理员选择医生、日期、时段,输入总号源数,系统自动初始化remain_number等于total_number。这个逻辑简单,但要注意一个坑:排班表要有unique约束(doctor_id, schedule_date, period),否则同一个医生同一天同一个时段会出现两条记录,挂号就会混乱。
2.2 预约挂号的防冲突核心
挂号的核心难点不在新增记录,而在"别超卖、别重复"。一张排班表剩余号数是10,两个患者同时挂号,必须保证只有一个人成功。从数据库层面,挂号表(registration)需要这些核心字段:
reg_id -- 挂号记录ID schedule_id -- 外键,指向排班表 patient_id -- 患者ID doctor_id -- 医生ID(冗余,查询方便) visit_date -- 就诊日期 period -- 时段 status -- 1待支付 2已支付 3已就诊 4已取消 5已退号 create_time -- 下单时间这里最关键的约束是:同一患者同一排班同一时段只能有一条有效挂号记录。所以除了普通的索引,建议加上联合唯一索引(schedule_id, patient_id, 但注意status字段要配合判断)。如果直接对三条字段做唯一索引,患者取消后再挂会插入失败,因此更稳妥的做法是通过加一个"逻辑唯一键":如schedule_id + patient_id + status IN (1,2,3)就不能用普通唯一索引了。实际项目中我推荐两种方案:
- 用select for update锁住排班记录行,先查状态再插入挂号记录;
- 用Redis预扣号源,异步同步数据库。
单体毕设用方案1即可,代码在第3章给。如果你用了Redis做预扣,记得处理超时未支付回滚号源的定时任务,否则号源会被"睡眠用户"占光。
2.3 电子病历的结构化设计
电子病历(medical_record)是所有业务里最需要思考扩展性的模块。因为病历内容既有结构化字段(主诉、现病史、既往史、初步诊断),又有非结构化内容(检查结果描述、医嘱备注)。我的设计建议是"主表 + 明细块":
record_id -- 病历ID reg_id -- 关联挂号记录 patient_id -- 患者ID doctor_id -- 接诊医生ID chief_complaint -- 主诉 present_illness -- 现病史 past_history -- 既往史 diagnosis -- 初步诊断 treatment_advice -- 治疗意见 create_time -- 书写时间现实医院里病历需要的字段远不止这些,但毕设和中小诊所场景,以上字段已经能覆盖80%的演示需求。这里有一个非常值得说的设计细节:**病历要不要和挂号记录建立一对一关联,是由接诊流程决定的。**一个挂号记录对应一份病历,就诊完成后就不能再次新建,只能编辑。所以挂号的status变成3(已就诊)时,就应该同时存在一条病历记录。为了防止"已就诊但没病历"的数据漏洞,我的建议是在服务层强制校验:更新挂号状态为已就诊时,用事务同时写入病历草稿记录,后续医生只需完善内容。
2.4 药品管理与库存联动设计
药品模块乍一看只是CRUD,实际上有两个细节容易被忽略:药品批号/规格的唯一性,库存流水要可追溯。
药品表(drug):
drug_id -- 药品ID drug_name -- 药品名称 specification -- 规格(如0.25g*24片) unit -- 单位(盒/瓶/袋) price -- 单价(Decimal类型,不要用Double) stock -- 当前库存 enabled -- 是否上架可用药品关联处方时,通过处方明细表(prescription_item)记录每次开药的数量。开药后要用事务扣减库存,同时写一条库存流水(stock_log),记录变更前数量、变更后数量、操作类型(入库/出库/盘点调整)、关联业务单号。这样一旦发现库存对不上,可以通过流水反查是哪个处方造成了差异,这在答辩演示时是一个很好的亮点素材。
有一点必须提醒:**价格字段绝不能用float或double计算。**金额是精确计算场景,必须用BigDecimal,否则会出现 0.1+0.2=0.30000000000000004 这种让人头大的问题。别问我怎么知道的,问就是项目里真出过事。
3. 关键功能实现与业务细节
3.1 排班生成逻辑实现
排班功能的管理端操作流程是:选择医生、日期、时段、总号数,点击生成。后端核心逻辑如下:
@Service public class ScheduleService { @Resource private ScheduleMapper scheduleMapper; @Transactional(rollbackFor = Exception.class) public void createSchedule(ScheduleCreateDTO dto) { int count = scheduleMapper.checkExist( dto.getDoctorId(), dto.getScheduleDate(), dto.getPeriod()); if (count > 0) { throw new BizException("该医生在当前时段已存在排班"); } Schedule schedule = new Schedule(); schedule.setDoctorId(dto.getDoctorId()); schedule.setDeptId(dto.getDeptId()); schedule.setScheduleDate(dto.getScheduleDate()); schedule.setPeriod(dto.getPeriod()); schedule.setTotalNumber(dto.getTotalNumber()); schedule.setRemainNumber(dto.getTotalNumber()); schedule.setStatus(1); scheduleMapper.insert(schedule); } }注意两个点:一是checkExist方法是配合数据库唯一索引的双重保险,单纯靠代码判断在高并发下有并发间隙风险;二是@Transactional必须加上,如果后续要扩展"生成排班的同时发送通知"之类逻辑,可以保证一致性。前端页面我建议用一个表格来展示一周排班,行是医生,列是星期一到星期日,单元格里显示上午/下午的号源情况,这个交互比下拉框直观得多,医生实时一看就知道哪天有空档。
3.2 挂号并发防超卖实现
这是整个项目技术含量最高的地方。我先给基于数据库锁的方案,这也是单体项目最稳妥的做法:
@Transactional(rollbackFor = Exception.class) public void register(RegisterDTO dto) { // 1. 锁排班记录,防止并发超卖 Schedule schedule = scheduleMapper.selectByScheduleIdForUpdate(dto.getScheduleId()); if (schedule == null || schedule.getStatus() != 1) { throw new BizException("该排班已停诊"); } if (schedule.getRemainNumber() <= 0) { throw new BizException("号源已约满"); } // 2. 幂等校验:同一患者同一排班同一时段只能挂一次 int duplicate = registrationMapper.checkDuplicate( dto.getScheduleId(), dto.getPatientId()); if (duplicate > 0) { throw new BizException("您已挂过该号,请勿重复挂号"); } // 3. 扣减号源 + 插入挂号记录(事务保证原子性) scheduleMapper.decreaseRemainNumber(dto.getScheduleId()); Registration reg = new Registration(); reg.setScheduleId(dto.getScheduleId()); reg.setPatientId(dto.getPatientId()); reg.setDoctorId(schedule.getDoctorId()); reg.setVisitDate(schedule.getScheduleDate()); reg.setPeriod(schedule.getPeriod()); reg.setStatus(1); reg.setCreateTime(new Date()); registrationMapper.insert(reg); }selectByScheduleIdForUpdate是关键,这行SQL在事务内对这条排班记录加了行级排他锁,其他事务想修改这条记录必须等当前事务提交。用生活类比:这就好比火车票售票窗口,前面的人还没买完,后面的人只能排队等,谁先到谁就锁住了这个窗口。实际压测场景中,100个并发请求同时抢1个号源时,最终只会有一个请求成功插入挂号记录,其余全部拿到剩余号数为0的业务异常。这个效果在答辩现场演示绝对是加分项。
如果项目使用了Redis,更优雅的方案是DECR预扣号源:
1. 排班生成时,将remainNumber同步到Redis key:schedule:remain:{scheduleId} 2. 挂号请求先执行 redis.decr(key),返回值 >= 0 说明抢号成功 3. 业务落库,异步发消息扣减数据库号源 4. 取消挂号时恢复 redis.incr(key)但要注意Redis方案需要解决数据库与Redis的一致性问题,比如Redis扣减成功但数据库事务失败,就要做补偿。对毕设而言,数据库锁方案已经完全够用且更好解释,我建议优先实现它,Redis方案作为扩展加分点提一嘴即可。
3.3 电子病历与处方联动实现
电子病历填写界面是医生端最常用的功能,我建议做成类似"问诊工作台"的布局:左边是患者基本信息与历史就诊记录,右边是病历填写表单,下方是处方开立区域。核心事务逻辑如下:
@Transactional(rollbackFor = Exception.class) public void saveRecordAndPrescription(RecordPrescriptionDTO dto) { // 1. 校验挂号状态必须是"已就诊"或"待就诊" Registration reg = registrationMapper.selectById(dto.getRegId()); if (reg == null || reg.getStatus() != 2) { throw new BizException("患者未支付挂号费用,不能就诊"); } // 2. 保存电子病历 MedicalRecord record = new MedicalRecord(); record.setRegId(dto.getRegId()); record.setPatientId(reg.getPatientId()); record.setDoctorId(reg.getDoctorId()); record.setChiefComplaint(dto.getChiefComplaint()); record.setDiagnosis(dto.getDiagnosis()); medicalRecordMapper.insert(record); // 3. 保存处方明细并扣减库存 for (PrescriptionItemDTO item : dto.getItems()) { Drug drug = drugMapper.selectByIdForUpdate(item.getDrugId()); if (drug == null || drug.getStock() < item.getQuantity()) { throw new BizException("药品库存不足:" + item.getDrugName()); } drugMapper.decreaseStock(item.getDrugId(), item.getQuantity()); // 写库存流水 stockLogMapper.insert(buildStockLog(drug, item.getQuantity())); // 写处方明细 prescriptionMapper.insert(buildPrescriptionItem(record.getRecordId(), item)); } // 4. 更新挂号状态为"已就诊" registrationMapper.updateStatus(dto.getRegId(), 3); }这段代码把三个最关键的环节串成了一个事务:病历落库、药品库存扣减、挂号状态流转。实际做的时候有两点要小心。第一,药品库存扣减和查询必须用selectByIdForUpdate,否则两个患者同时开同一个库存只剩1盒的药,就可能出现"两张处方都开成功但库存变成负数"的问题。第二,状态机必须严格:不能从"待支付"直接跳到"已就诊",如果写了这种代码,说明你对业务理解还有盲区,答辩老师一问就会露馅。
3.4 登录认证与权限控制的实现思路
这套系统的用户有三类:管理员、医生、患者。最简单实用的方案是用过滤器或拦截器做角色控制。你可以定义角色标识:
- 管理员:
ROLE_ADMIN - 医生:
ROLE_DOCTOR - 患者:
ROLE_PATIENT
登录成功后把用户ID和角色写入Session,然后写一个AuthInterceptor拦截器:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } // 按角色控制访问路径 String uri = request.getRequestURI(); if (uri.startsWith("/admin") && !"ROLE_ADMIN".equals(user.getRole())) { response.sendError(403); return false; } return true; } }如果你选的是前后端分离版本,可以把Session方案换成JWT:登录接口返回token,前端每次请求带Authorization头,后端用拦截器解析校验。这个方案面试官比较认可,因为它解决了跨域和前后端分离场景下的认证问题。毕设阶段用Session完全够了,毕竟部署简单、不容易出错。
4. 项目运行部署与常见问题排查
4.1 拿到源码后快速启动五步走
标题里说到了源码+文档+运行视频,很多零基础同学拿到后第一个问题就是"双击哪里能跑"。我第一次带学生时发现,光启动顺序就能卡住一半人,所以我习惯把步骤压缩成五条:
- 准备环境:安装JDK 1.8+(不要装JDK 17除非你想体验配置地狱)、Maven 3.6+、MySQL 5.7+、IDE(推荐IDEA)。
- 导入SQL:用Navicat或命令行执行项目doc目录下的
hospital.sql脚本,把数据库初始化好。注意调整脚本里的字符集为utf8mb4,否则中文会乱码。 - 修改配置:打开
application.yml,把数据库地址、账号、密码改成自己本机的。从报错来看,90%的运行失败都是这一步漏配。 - 启动后端:运行
HospitalApplication.java的main方法,看到"Started HospitalApplication"日志就说明启动成功。默认端口是8080,浏览器访问http://localhost:8080。 - 登录验证:用文档里提供的管理员账号登录,切换到医生端、患者端分别走一遍流程,确认功能正常。
这五步看似简单,但我还是建议按顺序走,不要跳步。尤其是"导入SQL"和"改配置"这两步,很多学员以为是可选项,结果启动时装数据源超时、表找不到,白白浪费一下午。
4.2 高频踩坑问题速查表
我把带项目时遇到的高频问题整理成了表格,一个个排查,节省的时间够你多看两集电视剧:
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 启动报"Failed to configure a DataSource" | application.yml数据源没配对 | 检查url、username、password,检查MySQL驱动依赖是否存在 |
| 端口被占用 | 本地已有服务占用8080 | 改server.port为8090或8100 |
| 访问页面中文乱码 | 数据库连接url缺少字符集参数 | url加?useUnicode=true&characterEncoding=utf8mb4 |
| 登录后页面跳转404 | 多个控制器路径冲突,或拦截器拦截了静态资源 | 检查拦截器是否放行js/css/图片路径,把静态资源目录加到排除名单 |
| 时间字段差8小时 | MySQL连接时区问题 | url加serverTimezone=Asia/Shanghai |
| MyBatis Plus分页失效 | 缺少分页插件配置 | 配置MybatisPlusInterceptor并添加PaginationInnerInterceptor |
| 药品价格计算出现小数位错误 | 用了Double/Float计算金额 | 全部改为BigDecimal,金额乘除法用multiply()和divide() |
| 挂号并发出现超卖 | 没加锁或锁的范围不对 | 使用selectByScheduleIdForUpdate行级锁,确保扣减和插入在同一个事务 |
这里重点说下端口占用,因为Windows下特别容易踩。命令行执行netstat -ano | findstr 8080找到占用进程的PID,然后taskkill /PID 进程号 /F。或者更粗暴的方式,直接改端口到8090,省得跟本地其他程序打架。
4.3 从"能跑"到"能答辩"的加分改造
很多人的项目做完就只停留在"能跑",但这跟答辩想要的高分还有距离。我建议按优先级做三个增量改造,性价比非常高:
**第一个,加Redis实现验证码和Token。**登录时生成图形验证码存Redis,设置2分钟过期;登录成功后生成随机Token存Redis,前端每次请求带Token鉴权。答辩时你可以说"用Redis解决了Session在集群环境下不共享的问题",这句话一说出来,评委就知道你不是纯粹的CRUD选手。
**第二个,加定时任务做排班关闭和号源恢复。**到了当天下午,系统自动把上午的过期排班状态置为停诊;如果预约挂号30分钟未支付,自动释放号源并标记挂号记录为已取消。用SpringBoot自带的@Scheduled即可实现,注意加上@EnableScheduling注解。这是很多医院系统的真实需求,做出来是真的能讲出业务价值的。
**第三个,加统计报表。**按科室统计每日挂号量、按医生统计接诊人次、按药品统计销售额,用ECharts画柱状图和折线图放管理端首页。有了这个,系统就从"能用"变成"管理工具",技术广度和业务深度同时体现出来。数据统计SQL也不复杂:
SELECT DATE_FORMAT(visit_date, '%Y-%m-%d') AS day, COUNT(*) AS reg_count FROM registration WHERE status IN (2, 3) GROUP BY DATE_FORMAT(visit_date, '%Y-%m-%d') ORDER BY day DESC LIMIT 7;这三个改造平均每个只需要半天到一天时间,但给项目的"丰满度"提升是肉眼可见的。我见过太多项目功能看着齐全,实际演示时10分钟就点完,没有任何延伸话题。加完这三块,演示时间和问答深度都能撑起来。
5. 贴一张我实际调试时的经验备忘录
写到这里想额外分享一个容易被忽略的点:这类项目做完之后,**千万别只在本机跑。**拍摄运行视频或者答辩演示的时候,尽量用一台干净环境或者虚拟机来跑,避免出现"我本机能跑但老师电脑上一堆报错"的尴尬。
打包部署的命令也给你准备好:
# 先执行测试跳过,再打包 mvn clean package -DskipTests # 启动jar包 java -jar target/hospital-system-1.0.0.jar --server.port=8081打包时如果出现资源文件没有打进去的问题,检查pom.xml里的resources配置,确保src/main/resources下的xml和yml都被包含。很多人用IDEA直接点绿色三角跑没问题,但一打包就出幺蛾子,就是这个原因。
另外,运行视频一般要求演示完整流程,我建议录屏时按这样一个顺序走:管理员登录→创建科室→创建医生→为医生排班→注册患者→患者预约挂号→支付→医生开病历→开药→库存变化→管理端统计查看。一条线走下来,整个系统的业务闭环全部展示清楚,讲解逻辑也顺,不会出现"讲到一半不知道接着点哪里"的情况。
讲解视频强调的不是讲代码,而是讲为什么这样设计。尤其建议在排班生成、挂号防并发、病历与处方事务一致性这三个地方多讲几句,把本章第3节的思路用自己的话复述一遍,就能让观看者明白你不只是会抄代码,而是真的理解了这个系统的运转规则。
我个人带项目有一个习惯:让学习者不要急着写代码,先拿A4纸把"挂号→就诊→开药→扣库存"这条链路画一遍,把每张表的主外键关系标出来,把状态流转的每个可能性列出来,再动手。这样做了之后,项目不仅完成得快,后续答辩被问倒的概率也会低非常多。特别是这类带复杂的排班、挂号、病历、药品联动的医疗系统,业务理解永远比代码语法更重要——代码只是把你想清楚的规则翻译成机器能执行的语言罢了。