☰
SpringBoot医院管理系统全栈实战:从数据库设计到部署交付
2026/10/7 3:05:49 网站建设 项目流程

一个医院管理系统,拿到手的第一步不是急着写代码,而是先想清楚它到底要解决什么问题。我做过几个医疗相关的项目,从门诊小诊所到二甲医院的信息化改造都有涉及,这类系统的核心从来不是技术炫技,而是把挂号、看诊、开药、收费、库存这几条线串起来,让数据在一个闭环里跑通。基于SpringBoot做医院管理系统,是目前中小型项目里性价比最高的一条路:轻量、生态成熟、招人好招、部署不折腾。

这篇文章我把整个项目的落地思路完整拆一遍,从功能规划、数据库设计、后端实现、前端联动到交付文档,全是我实际写代码和跑部署时验证过的东西。不管你是刚学SpringBoot想找个完整项目练手,还是需要给学校、公司交付一套带源码和文档的医院管理系统,这篇都能当参考底稿。

1. 项目落到地上之前,先把业务模型理清楚

1.1 医院管理系统的核心需求拆解

医院管理系统听起来很大,但拆开看就是几个固定的业务场景在循环:患者进门(挂号)、医生看病(诊断开方)、药房发药(库存变动)、收费结算(账单闭环)。这四个场景串起来,系统的骨架就出来了。

我第一次做类似项目的时候犯过一个典型错误:一上来就画了二十多张表,把住院、手术、体检全都塞进去,结果开发到一半发现根本驾驭不住。后来带团队做交付项目,我定了一条规矩:第一版只做门诊流程,每分钟都花在核心链路上。

具体到功能模块,一个能真正跑起来的SpringBoot医院管理系统至少需要这几块:

  • 系统管理:用户登录、角色权限、员工账号管理
  • 基础数据:科室维护、医生信息、药品目录
  • 门诊业务:挂号登记、医生看诊、处方录入
  • 药品管理:库存查询、入库出库、库存流水
  • 收费管理:处方结算、账单查询、退费处理

注意:很多教程项目会把系统管理放在最前,但实际业务里,挂号到收费这条主链路才是命脉,表设计和代码结构都要优先保证它走得通。

1.2 角色权限怎么设计才够用

医院系统里角色不能乱设计,权限控不住是会出问题的。参考实际业务,我一般拆成四类基础角色:

角色核心操作权限边界
管理员科室维护、医生排班、药品管理、数据统计系统全部功能
医生查看挂号患者、写诊断、开处方只能看自己的患者和自己的处方
药房人员药品查询、库存操作、处方发药不能改价格、不能改诊断
收费员收费结算、账单查询不能开处方、不能改药品

在SpringBoot里做权限,最直接的方式是登录后给用户存角色标识,后端接口用拦截器校验。项目规模不大时,不建议直接上Spring Security那套复杂的过滤器链,自己写个HandlerInterceptor,在preHandle里校验Token和角色,完全够用。当然,如果你后续要扩展成多医院、多租户,那再考虑引入Sa-Token或Spring Security,第一版真没必要。

1.3 为什么选SpringBoot而不是SSH或Spring Cloud

现在写新项目,我几乎无脑选SpringBoot。原因就三条:内嵌Tomcat省了部署配置、自动装配省了xml堆砌、生态整合省了找轮子的时间。如果你用早期的SSH(Struts+Spring+Hibernate),光配置文件就能看晕人,而SpringBoot一个启动类全搞定。

另外有个很实在的点:医院管理系统这种业务型项目,技术复杂度不在框架本身,而在业务逻辑的准确性和数据的一致性。SpringBoot能让你把精力集中在业务代码上,而不是跟配置打架。至于Spring Cloud那套微服务,单体应用阶段真没必要,等用户量上来、并发上来了再拆不迟,一开始就微服务反而把自己坑进去。

2. 数据库设计:这张表结构图是整个系统的地基

2.1 核心表结构逐张拆解

数据库设计是医院管理系统里最见功夫的环节。我直接把我这版项目里的核心表结构列出来,每张表都是实际能落地的:

用户表(sys_user):不要跟患者表混在一起。这个表存的是系统登录账号,包括管理员、医生、药房人员、收费员。

CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)', real_name VARCHAR(50) COMMENT '真实姓名', role VARCHAR(20) NOT NULL COMMENT '角色: ADMIN/DOCTOR/PHARMACIST/CASHIER', phone VARCHAR(20), status TINYINT DEFAULT 1 COMMENT '1正常 0禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

科室表(dept_info):医院的组织结构基础,医生表通过dept_id关联它。表字段不要多,dept_code、dept_name、location、intro就够。

医生表(doctor_info):这里有个细节,医生既要关联科室,又要关联用户表(因为医生要登录系统)。我习惯把医生基本信息放业务表,登录账号在sys_user里,两张表通过user_id关联。

CREATE TABLE doctor_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT COMMENT '关联sys_user.id', doctor_no VARCHAR(30) NOT NULL COMMENT '工号', name VARCHAR(50) NOT NULL, title VARCHAR(30) COMMENT '职称: 主任医师/副主任医师/主治医师', dept_id BIGINT COMMENT '所属科室', specialty VARCHAR(200) COMMENT '擅长领域' );

患者表(patient_info):注意身份证号是敏感字段,前端展示时要脱敏,数据库里可以明文存(业务需要),但接口返回要做处理。第一次来看诊的患者由挂号员建档,复诊的直接用卡号或身份证号查出来。

挂号表(registration):这是整个门诊流程的入口,字段要记录谁、在什么时间、挂了哪个科室哪个医生的号、状态是什么。状态机很重要:待就诊、就诊中、已完成、已退号,每个状态流转都要在代码里控制。

药品表(drug_info)和库存表:药品价格变更频繁,我建议单独建一个价格字段在drug_info里,但每次开处方的时候把当时的价格快照到处方明细表里,防止日后药品涨价导致历史账单对不上。

CREATE TABLE prescription_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, presc_id BIGINT NOT NULL COMMENT '处方id', drug_id BIGINT NOT NULL COMMENT '药品id', drug_name VARCHAR(100) COMMENT '药品名称(冗余)', price DECIMAL(10,2) COMMENT '开单时价格快照', num INT NOT NULL COMMENT '数量', dosage VARCHAR(200) COMMENT '用法用量说明' );

处方表(prescription):关联医生、患者、诊断结果、开方时间。一张处方对多条药品明细,这就是典型的主子表结构。

2.2 表关系设计的关键决策:冗余还是关联

做医院管理系统表设计,最纠结的就是要不要冗余。我的经验是:查询频繁且不常变化的字段,冗余;经常修改的字段,绝不冗余。

举个例子:处方明细表里冗余了drug_name和price,因为患者打印处方时不可能去join药品表再查一遍名称;但药品库存数字绝不冗余到别的表里,因为它是实时变化的,只能以drug_stock表为准。

另外,外键约束我建议代码里控制关联,数据库里不建物理外键。为什么?因为物理外键在删数据、导数据、做分库分表时会变成大麻烦,而且医院系统经常要补录历史数据,外键约束会让你很多SQL跑不动。用逻辑关联(也就是在service层校验存在性),既保证数据语义,又保留操作灵活性。

2.3 数据库脚本的交付规范

交付源码时,数据库脚本是别人能不能跑起来的关键。很多项目死在这一步:脚本不全、没数据、版本混乱。

我整理数据库脚本的习惯是分三个文件:

  • db_init.sql:建库建表语句,带完整的字段备注
  • db_data.sql:初始化数据,包括管理员账号、科室、医生、药品、演示患者
  • db_update.sql:后续迭代的增量变更

初始化数据这里有个必须注意的事:管理员密码不能明文写死在脚本里,要用BCrypt加密后的字符串。我见过太多项目初始化密码是123456明文,这个在演示项目里无所谓,但交付给真实医院就成安全事故了。SpringSecurity的BCryptPasswordEncoder生成一串放进去,登录时用同一个Encoder校验,这是标准做法。

3. 后端核心模块实现:代码怎么组织才不烂尾

3.1 项目分层结构与基础工程配置

SpringBoot项目分层我用了最标准的四层结构,后端工程名就叫hospital-server:

com.hospital ├── controller(接收请求、参数校验、返回结果) ├── service(业务逻辑、事务控制) │ └── impl ├── mapper(MyBatis-Plus的数据访问层) ├── entity(数据库实体) ├── dto(前端交互对象) ├── vo(视图对象,如登录返回的Token信息) ├── common(统一返回、全局异常、常量) └── config(拦截器、跨域、MyBatis-Plus配置)

工程配置文件里要提前解决几个容易埋雷的点:

MySQL连接串必须带时区参数。MySQL 8.x的驱动对时区很敏感,jdbc:mysql://localhost:3306/hospital?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=true,少了serverTimezone直接给你报The server time zone value异常,新手在这个问题上卡住的概率极高。

MyBatis-Plus的分页插件要单独配置。MyBatis-Plus 3.5.x的分页需要显式注册PaginationInnerInterceptor,不然Page对象查出来total永远是0:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

3.2 登录认证:JWT手写还是用框架

医院管理系统这种后台管理型项目,JWT是最合适的认证方案。它是无状态的,服务器不需要存session,对集群部署也友好。我自己写过一套轻量的JWT工具类,核心就三步:登录成功生成Token、拦截器校验Token、从Token里拿用户信息。

// 登录接口核心逻辑 public LoginVO login(LoginDTO dto) { SysUser user = sysUserMapper.selectOne( new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, dto.getUsername())); if (user == null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { throw new BusinessException("账号或密码错误"); } if (user.getStatus() == 0) { throw new BusinessException("账号已被禁用"); } String token = JwtUtil.generateToken(user.getId(), user.getRole()); return new LoginVO(token, user.getRealName(), user.getRole()); }

拦截器里校验Token,注意要放行登录接口和静态资源:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); // 校验token,失败则抛401异常 Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } }

踩坑提醒:跨域配置一定要在拦截器之前生效。如果先注册拦截器再配置CorsFilter,前端发OPTIONS预检请求会被拦截器拦掉,导致浏览器报跨域错误但后端逻辑根本没执行。解决方法是注册拦截器的时候设置excludePathPatterns放行OPTIONS,或者直接用WebMvcConfigurer.addCorsMappings处理跨域。

3.3 挂号与开处方:事务一致性怎么保证

医院系统里有一个典型的多表事务场景:挂号。患者挂号需要做三件事:查询号源是否充足、生成挂号记录、扣减医生当天的剩余号数。这三步要么全成功,要么全失败,所以必须加@Transactional。

@Transactional(rollbackFor = Exception.class) public RegistrationVO register(RegisterDTO dto) { // 1. 查询排班,判断号源 Schedule schedule = scheduleMapper.selectById(dto.getScheduleId()); if (schedule == null || schedule.getRemainNum() <= 0) { throw new BusinessException("号源不足"); } // 2. 创建挂号单 Registration reg = new Registration(); reg.setPatientId(dto.getPatientId()); reg.setDoctorId(schedule.getDoctorId()); reg.setVisitDate(schedule.getWorkDate()); reg.setFee(schedule.getFee()); reg.setStatus(0); // 待就诊 registrationMapper.insert(reg); // 3. 扣减号源 schedule.setRemainNum(schedule.getRemainNum() - 1); scheduleMapper.updateById(schedule); return new RegistrationVO(reg); }

开处方也一样,主体表和明细表必须在一个事务里。先插处方主表拿到自增id,再循环插入明细表。我遇到过新手把两步写在两个service方法里没加事务,结果处方主表有了、明细丢了,收费那边对不上账。

事务的另外一个大坑是方法自调用导致事务失效。this.xxx()调用同类里的一个带@Transactional的方法,事务是起不来的,必须通过注入自身的代理对象或者拆到另一个service类里调用。

3.4 统一返回和全局异常:让前端少写一百行判断

项目从第一天就要定接口规范,不然后端返回格式五花八门,前端联调就要疯了。我用统一返回类Result<T>,结构固定为code / message / data。code是业务码,0表示成功,非0表示各类业务错误。

全局异常处理用@RestControllerAdvice,把业务异常、参数校验异常、系统异常分别处理:

@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public Result<Void> handleBusinessException(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public Result<Void> handleValidException(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldErrors() .stream().map(FieldError::getDefaultMessage) .collect(Collectors.joining(";")); return Result.error(400, msg); } @ExceptionHandler(Exception.class) public Result<Void> handleException(Exception e) { log.error("系统异常", e); return Result.error(500, "系统繁忙,请稍后重试"); } }

这样做的价值是:前端只需要判断code是否为0,所有业务流程的错误提示都从message里拿,不用每个接口单独写错误分发逻辑。

4. 前端联调与部署:Vue打包塞进SpringBoot的那些细节

4.1 前端技术方案:独立Vue工程还是Thymeleaf模板

医院管理系统的前端,我强烈建议用Vue独立工程 + 接口联调的模式,而不是Thymeleaf服务端渲染。原因很简单:医院系统的操作界面是典型的管理后台风格,表格、弹窗、表单多,Vue组件化的方式写起来清晰太多,Element UI直接拿来用,后台管理页面该有的组件全都现成。

4.2 Vue打包放进SpringBoot的正确姿势

很多教程项目的演示方式是前端和后端分开部署,但完整的源码交付一般希望一个jar包跑起来,这就涉及把Vue打包产物放进SpringBoot。

实际操作分三步:

第一步:前端构建,执行npm run build,生成dist目录,里面是index.html和static资源。

第二步:把dist目录里的内容复制到SpringBoot工程的src/main/resources/static目录下。注意是内容复制进去,不是把dist整个文件夹丢进去,否则访问路径会多一层。

第三步:处理路由跳转问题。Vue的history模式刷新页面时,Tomcat会尝试找对应的物理路径,找不到就404。解决方案是配置一个转发控制器,把非接口路径全部转发到index.html:

@Controller public class PageForwardController { @GetMapping(value = {"/", "/login", "/register", "/patient", "/doctor", "/drug"}) public String forward() { return "forward:/index.html"; } }

这里的问题在于,如果前端有几百个路由,一个一个写太蠢。我的解决办法是:前端统一路由前缀,后端用拦截器判断。接口路径统一/api开头,静态资源请求直接返回,其余非/api请求转发到index.html。这样前端路由随意加,后端不用改任何一行代码。

4.3 联调阶段的前端代理配置

开发阶段前端工程跑在8080,后端服务跑在9090(记得改application.yml里的端口,别跟前端挤在一起)。跨域问题靠Vue的proxy代理解决:

// vite.config.js export default defineConfig({ server: { port: 8080, proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } } });

这样前端代码里写的请求全是/api/xxx,开发时代理到后端,打包部署时因为静态文件和接口同源,根本不用改代码。这个模式我在好几个项目里验证过,非常稳。

5. 交付文档:源码之外,这些文档才是项目的门面

5.1 一套完整交付该有哪些文档

源代码 + 数据库脚本 + 文档,这三件套是医院管理系统交付的标准配置。很多开发者只重视代码,文档随便写两句,这个很吃亏。项目交付方最看重的就是文档,因为验收和后续维护全靠它。

我的文档目录结构长这样:

docs/ ├── 1. 需求分析说明书.md ├── 2. 数据库设计说明书.md ├── 3. 接口文档.md ├── 4. 部署手册.md └── 5. 测试报告.md

5.2 每份文档的核心内容

需求分析说明书:别写空话。第一段写项目背景,第二段写角色定义,第三段用用例图或表格把功能点列出来,每条功能后面标注优先级。最关键是把业务规则讲清楚,比如退号必须在就诊开始前、退费必须关联原账单号。

数据库设计说明书:核心内容是ER图和表结构说明。每张表要写清楚:用途说明、字段含义、主外键关系、哪些字段冗余了为什么冗余。这里我把每个枚举字段的取值含义写进文档里,比如registration.status 0待就诊 1已完成 2已退号,不然后来接手的人完全看不懂数字是什么。

接口文档:我推荐直接在项目里集成Swagger(springdoc-openapi),生成在线接口文档,同时导出一份Markdown版随源码交付。Swagger注解写好后,接口文档自动跟进代码变化,不会像手工文档那样改完代码忘了改文档。

部署手册:这份最容易被忽略,但决定用户能不能把项目跑起来。要从零开始写:安装JDK 1.8+(或者对应版本的JDK)、安装MySQL、初始化数据库脚本、修改配置文件、打包启动。每一步给命令、给截图说明、给验证方法,比如"启动后访问http://localhost:9090,看到登录页即部署成功"。

5.3 README怎么写才能让人一眼看懂

README是一个项目的脸面。源码根目录的README.md我力求信息密度高:第一屏展示项目简介和效果图,然后是技术栈表格、目录结构、快速启动三步、默认账号列表。重点强调默认账号:管理员admin/123456,医生doctor/123456,药房pharmacist/123456。方便评审和验收人员快速进入系统体验。

6. 常见问题与排查技巧实录

6.1 SpringBoot版本太高引发的连锁问题

现在网上教程满天飞,但版本对不上是家常便饭。SpringBoot从2.x升到3.x,基线从Java 8跳到Java 17,很多老项目的依赖也要跟着动。如果你照着教程做2.x的项目,结果新建工程时选了SpringBoot 3.x,javax.servlet就变成jakarta.servlet了,一堆代码直接编译不过。

我的建议:做这个项目时直接锁定SpringBoot 2.7.x + Java 8,这是目前最稳的组合。等到你需要用GraalVM或者新特性,再考虑上3.x。pom文件里把版本号写死,不要用SNAPSHOT,防止依赖漂移。

6.2 MySQL连接和驱动问题速查

现象原因解决办法
启动报Public Key Retrieval is not allowedMySQL8驱动默认不允许公钥检索jdbc url加allowPublicKeyRetrieval=true
报Unknown character set或乱码字符集配置不一致url加characterEncoding=utf8,数据库和表都用utf8mb4
时区异常MySQL连接未指定时区url加serverTimezone=Asia/Shanghai
运行时报Table doesn't exist数据库脚本没执行或连错库检查url里的database name和db_init.sql执行情况

6.3 打包后MyBatis的XML文件丢失

如果你用XML写SQL而不是注解,打包后运行报Invalid bound statement (not found),十有八九是mapper XML文件没被编译到classes目录。maven默认只打包java目录下的.class,resources目录下的一般没问题,但如果你把xml放在了src/main/java下,就必须在pom里单独配置资源:

<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources> </build>

6.4 前端刷新404的终极解法

前面提过history模式刷新404的问题,这里我再补充一个完整方案。用拦截器统一转发,定义两个前缀判断:/api开头的不动,/assets和/favicon.ico也不动,剩下所有路径都forward到index.html。这个方案对Vue Router里任意深度的路由都有效,因为最终都是前端Router自己接管解析。

6.5 事务不生效的隐蔽场景

我在交付项目时帮人排查过一个诡异问题:批量导入药品时,前面几十条正常,中间某条数据非法,报错后前面正常插入的记录也保留下来了。一看代码,导入方法内部循环调用另一个service的方法,导入方法本身没加@Transactional。解决办法是给整个导入方法加事务,并且避免同类内this.xxx()方式的自调用。

7. 我刚做完这个项目的几点体感

最后聊几句实际操作下来的体会。做医院管理系统这类业务型项目,我最大的感受是:难点不在框架,而在业务规则的完备性和异常分支的覆盖。比如退号之后号源要恢复,比如收费之后药品库存要扣减,这些跨模块的数据流转才是真正需要花时间画流程图去理清楚的。

拿我最近一次交付的经验来说,项目主体开发其实只花了一周多,但测试和修数据一致性问题的耗时差不多翻倍。如果你是自己做毕业设计或者练手项目,建议多花时间在测试各种业务路径上:挂号缴费、退号退费、库存不足时开处方,这些边界场景全跑通,系统才算真正能用。另外建议你把数据库初始化脚本和README文档从一开始就同步维护,别等到交付前再补,补出来的文档基本都缺东少西。这套流程走完,交付出去的医院管理系统,不管是代码、数据库还是文档,都是能直接拿得出手的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询