简介:本资源是一套基于SpringBoot的医院信息管理系统毕业设计论文Java项目,面向计算机相关专业需要完成毕业设计的学生及Java初学者。系统采用B/S架构,以Java语言开发,MySQL作为后台数据库,涵盖首页、个人中心、用户管理、医生管理、科室管理、挂号信息管理、取消挂号管理、问诊记录管理、病房管理、药房管理及管理员管理等模块,论文完整包含绪论、开发环境、需求分析、概要设计、详细设计、系统测试及结论致谢等章节,并配有功能模块图、流程图与E-R图。资源包共1个docx文件,约4.48MB,内容为完整毕业论文文档,结构清晰,便于直接参考与修改。目前已有341人学习下载,适合需要快速搭建毕设框架、梳理开发流程与撰写论文的读者借鉴使用。
1. 从一份“能跑起来”的医院信息管理系统说起
很多做 Java 毕业设计或课程项目的开发者,第一次听到“基于 SpringBoot 医院信息管理系统”时,脑子里冒出来的往往是“这不就是个增删改查吗”。真动手才发现,挂号、门诊、住院、药房、收费这几条业务线一旦串起来,表结构能轻松突破三十张,状态流转更是绕得人头皮发麻。这个标题背后真正要解决的需求,是把医院里最核心的“人、号、单、药、钱”五件事用一套后台管起来,让挂号员、医生、药房管理员、收费员各自看到该看的界面,数据还能对得上。它适合两类人:一类是需要一个结构完整、能写进论文、能演示答辩的 Java Web 项目;另一类是想借这个场景把 SpringBoot + MyBatis-Plus + Vue 这套主流组合真正练一遍的开发者。下面我按自己带过几个类似项目的经验,把选型、建表、接口、权限和踩坑一条条讲清楚,你照着能复现出一个可演示、可扩展的版本。
2. 技术选型与模块拆分:为什么这套组合最稳
2.1 后端为什么锁定 SpringBoot + MyBatis-Plus
医院信息管理系统的业务特点是“表多、关联多、查询条件杂”。挂号要按科室、医生、日期查排班;门诊要按患者、时间段查病历;药房要按药品名、库存阈值查库存。如果用原生 JDBC 或 JPA,光是写动态查询条件就够喝一壶。MyBatis-Plus 的QueryWrapper和LambdaQueryWrapper在这种多条件组合查询场景下非常顺手,而且它自带的BaseMapper能省掉大量单表 CRUD 代码。
SpringBoot 的价值在于自动装配和起步依赖。一个spring-boot-starter-web加一个mybatis-plus-boot-starter,再配一个数据源,项目骨架就立起来了。我一般会选 SpringBoot 2.7.x 这个版本区间,因为它对 JDK 8 和 JDK 11 都友好,而很多学校的实验环境还停留在 JDK 8。如果你用 JDK 17,那就上 SpringBoot 3.x,但要注意javax.*要换成jakarta.*,这个坑后面会细说。
<!-- pom.xml 核心依赖,版本按自己环境微调 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>这段依赖里,spring-boot-starter-web负责 MVC 和内置 Tomcat,mybatis-plus-boot-starter负责 ORM 和分页插件,MySQL 驱动负责连接数据库。注意 MyBatis-Plus 3.5.x 和 SpringBoot 3.x 搭配时,要确认用的是mybatis-plus-spring-boot3-starter,否则启动会报NoClassDefFoundError,这是版本匹配的硬性要求。
2.2 前端为什么用 Vue + Element UI 而不是纯 Thymeleaf
如果只是交个课设,Thymeleaf 确实能少写很多代码。但医院信息管理系统的界面交互密度高:挂号页要联动科室和医生下拉框,药房页要弹窗批量入库,收费页要实时计算金额。这些用 Thymeleaf 做局部刷新会很别扭。Vue + Element UI 的表格、表单、弹窗组件开箱即用,配合 axios 调后端接口,前后端职责清晰,论文里也更好画架构图。
我一般会把前端拆成这几个页面模块:登录页、工作台、挂号管理、门诊管理、住院管理、药房管理、收费管理、系统管理。每个模块对应一个 Vue 文件,路由用 vue-router 管理,权限用路由守卫加后端接口双重校验。
2.3 模块拆分的三个原则
第一,按业务域拆包,不要按技术层拆。很多人喜欢controller一个包、service一个包、mapper一个包,结果一个挂号功能要翻三个包才能看全。我习惯按registration、outpatient、pharmacy这样拆,每个域下面再分 controller、service、mapper。
第二,公共能力抽到common包。比如统一返回体Result<T>、全局异常处理GlobalExceptionHandler、分页参数封装PageQuery,这些每个模块都要用。
第三,权限模型尽早定。医院系统里角色至少分管理员、挂号员、医生、药房管理员、收费员五种。我一般用 RBAC 模型,用户表、角色表、权限表加两张关联表,登录后把权限码塞进 token,后端用注解或拦截器校验。
3. 数据库设计:三十张表怎么砍到能维护
3.1 核心表清单与字段取舍
医院信息管理系统的表可以很多,但核心业务表其实就这些:用户表、角色表、权限表、科室表、医生表、排班表、患者表、挂号记录表、病历表、处方表、处方明细表、药品表、库存记录表、住院登记表、收费记录表。加起来十五张左右就能跑通主流程,论文里再补充字典表和日志表,凑到二十张上下比较合理。
以挂号记录表为例,关键字段包括:挂号编号、患者 ID、科室 ID、医生 ID、排班 ID、挂号类型(普通/专家/急诊)、挂号费、支付状态、挂号时间、就诊状态。这里有个容易翻车的地方:很多人把患者姓名、医生姓名直接冗余进挂号表,觉得查询方便。短期看确实省了 join,但患者改名或医生调岗后数据就对不上了。正确做法是只存 ID,查询时用 join 或前端二次请求补全。
CREATE TABLE `registration` ( `id` bigint NOT NULL AUTO_INCREMENT, `reg_no` varchar(32) NOT NULL COMMENT '挂号编号', `patient_id` bigint NOT NULL COMMENT '患者ID', `dept_id` bigint NOT NULL COMMENT '科室ID', `doctor_id` bigint NOT NULL COMMENT '医生ID', `schedule_id` bigint NOT NULL COMMENT '排班ID', `reg_type` tinyint NOT NULL DEFAULT 1 COMMENT '1普通 2专家 3急诊', `fee` decimal(10,2) NOT NULL COMMENT '挂号费', `pay_status` tinyint NOT NULL DEFAULT 0 COMMENT '0未付 1已付 2退费', `visit_status` tinyint NOT NULL DEFAULT 0 COMMENT '0待诊 1就诊中 2已完成', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_reg_no` (`reg_no`), KEY `idx_patient` (`patient_id`), KEY `idx_doctor_date` (`doctor_id`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表时注意三点:reg_no加唯一索引防止重复挂号;patient_id和doctor_id加普通索引加速查询;create_time默认值用CURRENT_TIMESTAMP省去手动赋值。字符集统一utf8mb4,否则患者姓名里的生僻字会变问号。
3.2 状态字段用数字还是字符串
我见过两种做法。用数字(0/1/2)的好处是省空间、索引快,坏处是可读性差,查数据库时得对着文档猜。用字符串(WAITING/PAID)的好处是直观,坏处是容易写错拼写。我的习惯是数据库存数字,Java 里用枚举映射,前端用字典翻译。这样三层各取所需,也方便后续加状态。
public enum VisitStatus { WAITING(0, "待诊"), IN_PROGRESS(1, "就诊中"), FINISHED(2, "已完成"); private final int code; private final String desc; // 构造和 getter 省略 }枚举里 code 和数据库对应,desc 给前端展示。新增状态时只改枚举和字典表,不用动业务逻辑。
3.3 排班表与挂号表的联动设计
排班表记录医生某天上午或下午在哪个科室出诊、限号多少、已挂多少。挂号时先查排班剩余号源,有号才允许挂号,同时把排班的已挂数加一。这两个操作必须在一个事务里,否则并发下会超卖。
@Transactional(rollbackFor = Exception.class) public Result<?> register(Long scheduleId, Long patientId) { // 悲观锁查排班,防止并发超卖 Schedule schedule = scheduleMapper.selectByIdForUpdate(scheduleId); if (schedule.getRegistered() >= schedule.getTotal()) { return Result.fail("号源已满"); } schedule.setRegistered(schedule.getRegistered() + 1); scheduleMapper.updateById(schedule); // 后续插入挂号记录... return Result.ok(); }selectByIdForUpdate对应 SQL 里的FOR UPDATE,把这一行锁住直到事务提交。代价是并发高时会有锁等待,但医院挂号场景并发量不大,这个方案足够稳。如果不想用悲观锁,也可以用 Redis 预减库存,但那就引入额外组件了,课设阶段没必要。
4. 核心接口实现:挂号、门诊、药房三条线
4.1 挂号接口的分页查询与多条件组合
挂号列表页通常要支持按患者姓名、科室、日期范围、就诊状态筛选。用 MyBatis-Plus 的LambdaQueryWrapper可以很优雅地拼条件。
public Page<RegistrationVO> pageQuery(RegistrationQuery query) { LambdaQueryWrapper<Registration> wrapper = new LambdaQueryWrapper<>(); // 患者姓名模糊查,需要 join 患者表,这里用子查询简化 if (StringUtils.hasText(query.getPatientName())) { List<Long> patientIds = patientMapper.selectIdsByNameLike(query.getPatientName()); wrapper.in(CollectionUtils.isNotEmpty(patientIds), Registration::getPatientId, patientIds); } wrapper.eq(query.getDeptId() != null, Registration::getDeptId, query.getDeptId()); wrapper.ge(query.getStartTime() != null, Registration::getCreateTime, query.getStartTime()); wrapper.le(query.getEndTime() != null, Registration::getCreateTime, query.getEndTime()); wrapper.eq(query.getVisitStatus() != null, Registration::getVisitStatus, query.getVisitStatus()); wrapper.orderByDesc(Registration::getCreateTime); Page<Registration> page = registrationMapper.selectPage(new Page<>(query.getPageNum(), query.getPageSize()), wrapper); // 转换为 VO,补全患者姓名、医生姓名、科室名称 return convertToVoPage(page); }这里的关键是每个条件都带一个布尔参数,条件为空时自动跳过,避免拼出WHERE dept_id = null这种查不到数据的 SQL。患者姓名走子查询是因为跨表模糊查用 join 加like在数据量大时索引会失效,先查 ID 再in反而更快。转换 VO 时批量查患者、医生、科室,避免 N+1 查询。
4.2 门诊病历与处方的保存逻辑
医生接诊后要写病历、开处方。病历和处方是一对多关系,一张处方对应多条药品明细。保存时要么全成功要么全回滚。
@Transactional(rollbackFor = Exception.class) public Result<?> saveMedicalRecord(MedicalRecordDTO dto) { MedicalRecord record = new MedicalRecord(); BeanUtils.copyProperties(dto, record); medicalRecordMapper.insert(record); // 保存处方明细 for (PrescriptionItem item : dto.getItems()) { item.setRecordId(record.getId()); // 校验库存 Drug drug = drugMapper.selectById(item.getDrugId()); if (drug.getStock() < item.getQuantity()) { throw new BizException("药品[" + drug.getName() + "]库存不足"); } prescriptionItemMapper.insert(item); // 扣减库存 drug.setStock(drug.getStock() - item.getQuantity()); drugMapper.updateById(drug); } return Result.ok(); }注意库存校验和扣减放在同一个事务里,任何一条药品库存不足就抛异常回滚,前面的病历插入也会撤销。这里用BizException自定义业务异常,配合全局异常处理器返回友好提示,而不是把堆栈直接抛给前端。
4.3 药房入库与库存预警
药房管理员需要批量入库,同时系统要在库存低于阈值时给出预警。入库记录单独一张表,每次入库都插一条流水,药品表的库存字段做累加。
public Result<?> batchStockIn(List<StockInDTO> list) { for (StockInDTO dto : list) { StockRecord record = new StockRecord(); record.setDrugId(dto.getDrugId()); record.setQuantity(dto.getQuantity()); record.setType(1); // 1入库 2出库 record.setOperatorId(SecurityUtils.getCurrentUserId()); stockRecordMapper.insert(record); // 更新药品库存 drugMapper.increaseStock(dto.getDrugId(), dto.getQuantity()); } return Result.ok(); }库存预警用一个定时任务或查询时判断都行。我一般在前端药品列表页加一列,库存低于阈值时标红,后端返回时带上warning布尔字段。这样不用额外跑定时任务,实现简单。
5. 权限与登录:别让越权毁掉整个项目
5.1 JWT 登录与角色校验
登录成功后生成 JWT,payload 里放用户 ID、角色码、权限码列表。前端每次请求把 token 放 header,后端拦截器解析并塞进ThreadLocal,业务层用SecurityUtils取当前用户。
public String login(String username, String password) { User user = userMapper.selectByUsername(username); if (user == null || !passwordEncoder.matches(password, user.getPassword())) { throw new BizException("用户名或密码错误"); } List<String> perms = permissionMapper.selectCodesByUserId(user.getId()); Map<String, Object> claims = new HashMap<>(); claims.put("userId", user.getId()); claims.put("role", user.getRoleCode()); claims.put("perms", perms); return JwtUtil.generate(claims, expireSeconds); }密码用 BCrypt 加密存储,不要用 MD5,MD5 彩虹表一查就出来。JWT 的过期时间我一般设两小时,太短用户老要重新登录,太长泄露风险大。权限码列表放 token 里省去每次查库,但用户权限变更后要等 token 过期才生效,这个取舍要跟需求方说清楚。
5.2 接口级权限注解
在 Controller 方法上加自定义注解,拦截器或 AOP 校验权限码。
@RequiresPerm("pharmacy:stock:in") @PostMapping("/stock/in") public Result<?> stockIn(@RequestBody List<StockInDTO> list) { return pharmacyService.batchStockIn(list); }@RequiresPerm是自定义注解,AOP 切面里取当前用户权限码列表,判断是否包含注解值。这样权限控制跟业务代码解耦,加权限只改注解,不用动逻辑。
5.3 数据权限:医生只能看自己的患者
功能权限管的是“能不能进这个页面”,数据权限管的是“能看哪些数据”。医生登录后,门诊列表只应显示自己的接诊记录。实现方式是在查询条件里自动拼上当前医生 ID。
// 在 service 层根据当前角色动态加条件 if (SecurityUtils.hasRole("DOCTOR")) { wrapper.eq(MedicalRecord::getDoctorId, SecurityUtils.getCurrentUserId()); }这个判断不要写死在每个查询里,可以抽成一个DataScopeHelper,或者用 MyBatis-Plus 的拦截器统一处理。课设阶段手动加几处也能接受,但论文里最好提一下数据权限的设计思路。
6. 避坑与排查:那些让我熬夜的瞬间
6.1 时间字段前后端差 8 小时
现象:前端显示的就诊时间比实际早或晚 8 小时。原因:数据库时区和 JVM 时区不一致,或者 JSON 序列化时没指定时区。解决:在application.yml里配spring.jackson.time-zone=GMT+8,数据库连接串加serverTimezone=Asia/Shanghai,实体类时间字段用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")。三处统一,基本不会再飘。
6.2 分页插件不生效,查出来是全量
现象:传了 pageNum 和 pageSize,返回的还是所有数据。原因:MyBatis-Plus 的分页插件没注册,或者注册了但版本不匹配。解决:新建配置类,注册MybatisPlusInterceptor并添加PaginationInnerInterceptor。
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }注意DbType.MYSQL要跟实际数据库一致,用错方言分页 SQL 会拼错。
6.3 事务不生效,库存扣了但记录没插
现象:方法上加了@Transactional,但异常时部分数据已提交。原因:方法被同类内部调用,Spring AOP 代理没生效;或者异常类型不在回滚范围内。解决:确保事务方法通过代理对象调用,或者把方法拆到另一个 Service;@Transactional默认只回滚RuntimeException,如果抛的是受检异常,要加rollbackFor = Exception.class。
6.4 前端跨域请求被拦
现象:本地开发时前端 8080 调后端 8081,浏览器报 CORS 错误。原因:后端没配跨域。解决:加全局跨域配置,或者用 Nginx 反向代理统一端口。开发阶段我一般加配置类。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }生产环境不要把allowedOriginPatterns设成*且allowCredentials为 true,浏览器会拒绝,而且有安全风险。上线时改成具体域名。
6.5 JDK 17 下 javax 包找不到
现象:升级到 SpringBoot 3.x 后,javax.servlet、javax.annotation全部报红。原因:Jakarta EE 9 之后包名从javax.*迁到jakarta.*。解决:全局替换 import,javax.servlet换jakarta.servlet,javax.annotation换jakarta.annotation。如果用了旧版第三方库还没适配,要么降 SpringBoot 版本,要么找替代库。
7. 进阶技巧:让项目在答辩时多拿几分
如果你已经把主流程跑通,想让项目看起来更完整,可以加这几个点。第一,操作日志。用 AOP 拦截带自定义注解的方法,记录操作人、时间、参数、结果,存到日志表。答辩时演示“谁在什么时候改了哪条数据”,比单纯说“我做了权限”有说服力。
第二,数据导出。挂号记录、收费记录支持导出 Excel,用 EasyExcel 几行代码就能实现。注意导出时也要走数据权限,医生只能导自己的患者。
@GetMapping("/export") public void export(HttpServletResponse response, RegistrationQuery query) { List<RegistrationVO> list = registrationService.listForExport(query); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=registration.xlsx"); EasyExcel.write(response.getOutputStream(), RegistrationVO.class).sheet("挂号记录").doWrite(list); }第三,接口文档。用 Knife4j 或 SpringDoc 自动生成接口文档,前端联调和答辩演示都方便。注意 SpringBoot 3.x 要用 SpringDoc 2.x,Knife4j 也要选对应版本。
第四,压力测试。用 JMeter 对挂号接口跑 100 并发,看有没有超卖、响应时间多少。把结果截图放进论文,比空谈“性能良好”强得多。我一般会测出 TPS 和平均响应时间两个数,再分析瓶颈在数据库锁还是连接池。
最后说个血泪经验:别等到答辩前一周才整合前后端。我见过太多项目,后端接口写完了,前端页面还是静态的,最后三天通宵联调,越调越乱。正确节奏是每完成一个模块就联调一个模块,哪怕界面丑一点,数据能跑通最重要。数据库脚本、接口文档、部署说明这三样从第一天就维护,最后写论文时直接整理,能省一半时间。希望帮到你。
本文还有配套的精品资源,点击获取