☰
Java医院设备管理系统拆解:Spring Boot与MyBatis-Plus的业务实践源码解析
2026/10/26 1:01:58 网站建设 项目流程

简介:在Java后端开发中,Spring Boot与MyBatis-Plus的组合已成为构建企业级业务系统的经典方案。一个可落地的管理系统,往往需要在数据层解决逻辑删除与唯一索引冲突,在业务层用状态机约束流程流转。以医院设备管理场景为例,设备台账、维修工单、报废审批等模块,本质上是对设备状态和流程节点的持续管理。通过阅读这套源码,可以理解Controller-Service-Mapper三层架构的职责划分,掌握@TableLogic实现数据可追溯、MetaObjectHandler自动填充审计字段等核心机制。这些技术设计不仅适用于医疗场景,也广泛支撑着仓储、行政等领域的资源管理系统。本文围绕一套可运行的Java医院设备管理系统源码,从架构设计、核心业务到环境排错,完整展示了Spring Boot项目从理论到工程的落地路径,为课程设计和科室二开提供了直观的参考。

1. Java医院设备管理系统:一份科室台账与报修流程都能落地的源码

设备科同事打电话说呼吸机报修了,你在Excel里翻半天找不到这台设备是哪年买的、在哪个病区、保修期到没到——这种场景在中小医院太常见了。这套Java医院设备管理系统源码,本质上就是把设备台账、维修工单、报废审批、科室领用串起来的一套 Spring Boot 管理后台。它不是一个花哨的大屏项目,而是能直接跑起来、能录入真实数据的业务系统,刚好覆盖设备科最核心的“这台设备现在在哪、什么状态、谁在修、能不能报废”。适合三类人:做 java 课程设计案例源码参考的学生、要给科室搭建信息化台账的医院信息科同事,以及想读 Spring Boot + MyBatis-Plus 业务代码的 Java 入门者。下面按我实际拆这个项目的顺序,从架构、业务到排错,完整讲一遍。

2. 拆架构:Spring Boot + MyBatis-Plus 的分层与实体映射

拿到源码第一步不是点运行,而是先看包结构。这套系统用的是最标准的 Controller-Service-Mapper 三层,没有过度设计。对二开来说,这种结构意味着你能顺着一条请求路径把代码读完,也能在不影响其他模块的前提下替换某个实现。

2.1 三层结构:请求怎么从页面走到数据库

我习惯把后端工程当成一条流水线看。Controller 层只做参数接收和结果包装,Service 层承载业务规则,Mapper 层只负责和数据库对话。三层职责分开之后,修改“报废审批必须经科长同意”这类规则时,只需要动 Service,不用去翻 SQL;优化一条慢查询时,也只需要动 Mapper,不会牵扯到接口签名。

src/main/java └── com/medical/equipment ├── controller │ ├── EquipmentController.java │ ├── RepairOrderController.java │ └── ScrapController.java ├── service │ ├── EquipmentService.java │ └── impl/EquipmentServiceImpl.java ├── mapper │ ├── EquipmentMapper.java │ └── RepairOrderMapper.java ├── entity │ ├── Equipment.java │ └── RepairOrder.java ├── common │ ├── Result.java │ └── PageResult.java └── EquipmentApplication.java

包名可以按你的公司域名换,但分层骨架不建议动。Controller 命名直接对应业务对象,Service 和 Mapper 同名后缀,新接手的人看到EquipmentController -> EquipmentService -> EquipmentMapper这条链,几乎不用猜文件在哪。这套源码里还有个common包,里面是统一返回结构Result和分页封装PageResult,前端所有请求都走这套结构,二开时不要再为某个接口单独造返回格式。

2.2 实体映射:@TableName 与驼峰转换

设备表在 MySQL 里叫equipment,Java 实体叫Equipment。这种对应关系完全符合 MyBatis-Plus 的默认规则:表名下划线转实体驼峰,开启map-underscore-to-camel-case就能自动映射device_code -> deviceCode。看下面这段实体代码。

@TableName("equipment") public class Equipment { @TableId(type = IdType.ASSIGN_ID) private Long id; private String deviceCode; private String deviceName; private String model; private String manufacturer; private Long departmentId; private Integer status; private LocalDateTime purchaseDate; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableLogic private Integer deleted; }

这段代码里最值得关注的是三个注解。@TableId(type = IdType.ASSIGN_ID)表示主键由 MyBatis-Plus 的雪花算法生成,不用依赖数据库自增,这样在数据迁移、分库时不会撞主键。@TableField(fill = FieldFill.INSERT_UPDATE)配合 6.2 节要讲的 MetaObjectHandler,会在插入和更新时自动写入时间戳,业务代码里完全不需要手动 setupdateTime。@TableLogic是关键中的关键,它让deleteById变成逻辑删除,底层执行的是UPDATE equipment SET deleted = 1 WHERE id = ?,后面查数据时所有 SQL 都会自动追加AND deleted = 0。这套机制直接解决了医院设备数据“误删无法追溯”的痛点,报废记录不会从库里消失。

2.3 分页参数:前端表格怎么和后端 Page 对齐

这套源码的前端用的是一套表格组件,分页参数约定俗成:页码pageNum、每页条数pageSize。后端只需要在 Controller 里声明Page<Equipment> page作为参数,MyBatis-Plus 的Page对象就能直接接住这些参数。让我有点意外的是,这套源码在分页插件配置上留了完整注释,照着配置就能用。

@GetMapping("/list") public Result listEquipment(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, String deviceName, Integer status) { Page<Equipment> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Equipment> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(deviceName), Equipment::getDeviceName, deviceName) .eq(status != null, Equipment::getStatus, status) .orderByDesc(Equipment::getUpdateTime); return Result.ok(equipmentService.page(page, wrapper)); }

LambdaQueryWrapper是这套代码里出现频率最高的类。like第一个参数是布尔判断条件,deviceName为空时整个条件自动跳过,不会再出现“查全部设备时莫名带上 where device_name like '%%'”这种低效查询。eq同理,status为 null 时不拼接该条件。这个写法的好处是查询条件天然可组合,不需要拼一串 if 去动态构造字符串。前端表格传pageNum=1&pageSize=10&deviceName=呼吸机,后端返回的Page对象里就有records、total、current这些字段,前端直接渲染即可。这里也体现了一个 Java 基础问题——动态条件查询是面试里常考的“如何避免 SQL 注入”,用参数化查询而不是字符串拼接,就是答案。

3. 核心业务落地:设备台账、维修工单、报废流程的状态流转

看管理系统的第一眼先看表设计,第二眼看状态字段。这套源码里所有业务都围绕“设备状态”展开:一台设备从入库到报废,会经历在用、维修中、停用、报废等状态。状态如果散落在代码各处,二开就变成一场灾难;反过来,状态用枚举收敛起来,整个业务逻辑就好读了。

3.1 设备台账表:字段怎么设计才不返工

设备台账不是简单的一张设备信息表。实际科室里同一型号的呼吸机可能买了很多台,每台都有独立的设备编号、所在科室、购买日期、保修截止日。如果把这些信息都塞进一张宽表,后续加“保养记录”“计量检测”这类扩展就只能继续加字段,直到表变成没人敢碰的怪物。这套源码的做法是把核心台账表做薄,把扩展业务拆到独立表。核心表字段如下。

CREATE TABLE equipment ( id BIGINT PRIMARY KEY COMMENT '主键,雪花ID', device_code VARCHAR(32) NOT NULL COMMENT '设备编号,唯一', device_name VARCHAR(64) NOT NULL COMMENT '设备名称', model VARCHAR(64) COMMENT '型号', manufacturer VARCHAR(64) COMMENT '生产厂家', department_id BIGINT COMMENT '所属科室ID', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 1在用 2维修中 3已报废 0停用', purchase_date DATE COMMENT '购买日期', warranty_end DATE COMMENT '保修截止日期', create_time DATETIME COMMENT '创建时间', update_time DATETIME COMMENT '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0正常 1删除', KEY idx_status (status), KEY idx_department (department_id), KEY idx_device_code (device_code) ) COMMENT '设备台账表';

status字段是整张表的风向标,所有列表查询、统计报表、工单流程都依赖这个值。这里我用的是TINYINT,而不是字符串枚举,原因有两个:一是存储和索引效率更高,二是后端用枚举常量类来映射数字含义,代码里不会出现魔法数字。idx_status和idx_department两个普通索引非常关键,因为最常见查询就是“某个科室下所有在用设备”,走联合索引或单列索引能明显快一些。device_code加唯一索引是为了防止重复建档——但注意,加了逻辑删除字段之后,唯一索引要谨慎,后面第 5.5 节会专门讲这个坑。

3.2 维修工单:状态机才是工单模块的灵魂

维修工单是设备管理里最容易写乱的模块。初学阶段的同学往往用一堆 if-else 判断“当前状态能不能做下一步”,状态一多,条件就乱成一团。这套源码用枚举把工单状态收敛成一个状态机,每个状态只允许固定的流转路径,非法流转直接抛业务异常。这种写法既好读,也禁得住业务方反复改需求。

public enum RepairStatusEnum { PENDING(1, "待派单"), ASSIGNED(2, "维修中"), PENDING_ACCEPT(3, "待验收"), CLOSED(4, "已关闭"); private final Integer code; private final String desc; private static final Map<Integer, List<Integer>> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(1, List.of(2)); TRANSITIONS.put(2, List.of(3)); TRANSITIONS.put(3, List.of(4)); } public static void validateTransition(Integer current, Integer target) { if (current.equals(target)) { return; } List<Integer> allowed = TRANSITIONS.get(current); if (allowed == null || !allowed.contains(target)) { throw new BusinessException("非法状态流转: " + current + " -> " + target); } } }

TRANSITIONS这个静态 Map 是关键:它把状态流转规则集中在一处定义,后续要加“加急结束”状态,只改这个 Map 就行。validateTransition会在 Service 层每次更新工单状态前被调用,传当前状态和期望状态,不合法直接抛异常,前端会收到统一错误提示。这个设计我建议读源码时重点看,它不仅适用于维修工单,科室领用、报废审批都能复用。兜底原则是:状态流转必须显式声明,不能靠“顺序执行”暗地里碰运气。

3.3 报废审批:流程不能是简单的 update

设备报废比维修更敏感,一台几十万的设备报废要经过科室申请、设备科审核、分管院长审批。如果业务代码里直接写UPDATE equipment SET status = 3,那这条操作既没有审批痕迹,也追责无门。这套源码把报废拆成两个动作:生成报废申请单 + 更新设备状态,审批通过之后才允许改状态。核心流程用表格表示如下。

阶段操作角色状态变化记录表
科室提交申请科室管理员设备状态不变,生成申请单scrap_apply (status = 1)
设备科初核设备科人员申请单状态 1 -> 2scrap_apply
分管院长审批院领导申请单状态 2 -> 3scrap_apply
报废执行设备科人员设备状态 -> 3equipment + scrap_apply

这里最关键的设计是:设备状态在审批通过前始终保持在原状态,申请单表和设备表通过equipment_id关联。审批流走得再久都不影响设备在科室里的日常使用记录,只有最后一步才把equipment.status改成已报废。这套源码的报废模块还留了一个字段scrap_reason,用来存审批意见,这在实际追溯里太重要了。

4. 跑起来:数据库初始化、配置项与启动排错路径

源码能不能跑通,七成取决于环境,三成取决于数据库初始化。很多课程设计源码下载之后启动失败,不是因为代码有问题,而是 MySQL 版本不兼容、JDK 没配好、SQL 脚本没导入干净。这一章按我实际跑通的顺序走一遍。

4.1 环境准备:JDK 版本与 Java 环境变量配置

先确认两个基础环境:JDK 和 Maven。这套源码基于 Spring Boot,JDK 1.8 以上基本都能跑,但建议直接用 JDK 8 或 JDK 11,太新的 JDK 版本反而可能在依赖解析时遇到意外问题。Maven 负责拉依赖,第一次编译会下载大量 jar,网络不好的地方要有耐心。Windows 上最常翻车的就是 Java 环境变量配置。

# 验证 JDK 是否生效 java -version # 验证 Maven 是否生效 mvn -v

如果java -version提示“不是内部或外部命令”,说明JAVA_HOME或PATH没配好。我一般会在JAVA_HOME填 JDK 安装根目录,然后在Path里追加%JAVA_HOME%\bin。配置完一定要新开一个 cmd 窗口再验证,因为旧窗口不会刷新环境变量。Maven 同样要确认MAVEN_HOME和Path。这套源码没有内置 Maven Wrapper 的话,本地 Maven 版本建议 3.6 以上,否则个别依赖版本解析会出奇怪问题。

4.2 数据库初始化:按脚本顺序导入

源码包里一般会有sql目录,里面是分模块的初始化脚本。有人贪快直接双击整个脚本,结果表依赖顺序错误,外键约束先于主表创建,直接报错。正确做法是按文件名字母序或说明文档指定的顺序导入。我这次遇到的是01_schema.sql、02_data.sql这样的命名,按顺序用source执行即可。

mysql -u root -p source C:/hospital_equipment/sql/01_schema.sql; source C:/hospital_equipment/sql/02_data.sql;

01_schema.sql里是建库建表语句,02_data.sql里是初始化数据,包括管理员账号、菜单权限、几台示例设备。这里要留意字符集:脚本里如果没有显式CHARSET=utf8mb4,导入后中文可能变成问号,强烈建议建库时手工指定。SQL 脚本里的department表是科室字典,后面新增设备时要用的科室下拉框就是从这张表查的。管理员账号初始密码如果脚本里没给 MD5 盐值,一般会在 README 里说明,比如admin / 123456的 MD5 值,进去后第一件事改密码。

4.3 修改数据源与自定义配置

数据库初始化完成后,打开src/main/resources/application.yml,改三处:数据库地址、用户名、密码。这套源码里默认的库名是hospital_equipment,账号是root,密码需要改成你本机的。除此之外,MyBatis-Plus 的配置也集中在这个文件里,逻辑删除、驼峰映射都在这里全局开启。

spring: datasource: url: jdbc:mysql://localhost:3306/hospital_equipment?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

url里那段characterEncoding=utf8mb4很关键,少了它,设备名称里生僻字可能全部变成乱码。allowPublicKeyRetrieval=true是 MySQL 8 连接时常见的必填项,否则报错Public Key Retrieval is not allowed,详情见第 5.1 节。logic-delete-field是全局逻辑删除开关,配置之后所有实体的deleted都自动生效,不用每个 XML 手动写条件。mapper-locations指向的 XML 目录放的是复杂查询 SQL,比如维修记录里跨三张表的统计报表。

4.4 启动方式与访问验证

配置改完就可以启动了。两种常见方式:一种是在 IDEA 里直接运行EquipmentApplication的 main 方法,适合开发调试;另一种是打成 jar 包用命令行启动,适合部署在服务器上。二开阶段用第一种,我一般是改完代码直接点 IDEA 的运行按钮。

# 方式一:maven 插件启动 mvn spring-boot:run # 方式二:打包后启动 mvn clean package -DskipTests java -jar target/equipment-system.jar

启动日志看到Started EquipmentApplication就说明启动成功,默认端口一般是8080。浏览器访问http://localhost:8080/,会跳转到登录页。用初始化账号登录后,先看“设备台账”菜单有没有示例数据,再点一下“维修工单”看看列表是否正常渲染。如果页面一直转圈,按 F12 看 Network 面板,多半是接口 404 或 500,顺着控制台日志查,比瞎猜快得多。这里提醒一句,IDEA 里运行如果遇到端口被占,找到被占用的进程关掉,或者在application.yml里改掉server.port。

5. 避坑记录:五个环境与业务层面的真实翻车

这套项目我在不同机器上跑过几次,每次遇到的问题几乎都能归结到下面五个点。每一条我都按“现象 -> 原因 -> 解决”写清楚,二开时遇到同类问题可以少走很多弯路。

5.1 MySQL 8 连接报错:Public Key Retrieval is not allowed

  • 现象:启动时报SQLNonTransientConnectionException: Public Key Retrieval is not allowed,但数据库账号密码确认无误。
  • 原因:MySQL 8 默认使用caching_sha2_password认证插件,客户端第一次连接时需要获取服务端公钥,而 JDBC URL 没允许这个动作。
  • 解决:在数据源 URL 末尾追加allowPublicKeyRetrieval=true&useSSL=false,两个参数缺一不可;useSSL=false是避免本机开发时 SSL 握手带来的额外延迟和证书报错。

5.2 日期参数 400:LocalDate 与字符串格式不对齐

  • 现象:前端传purchaseDate=2024-06-30,后端LocalDate接收直接 400,报Failed to convert value。
  • 原因:Spring MVC 默认日期转换格式不是yyyy-MM-dd,当请求参数格式和默认格式不一致时转换失败。
  • 解决:约定前后端统一格式,在 Controller 的日期类型字段上加@DateTimeFormat(pattern = "yyyy-MM-dd"),或者注册一个全局 Jackson 配置。我倾向全局配置,这样不用每个字段都加注解。这也是 java 相关项目里最常见的参数坑。

5.3 查询结果大量 null:驼峰映射没生效

  • 现象:数据库字段是device_code,实体属性是deviceCode,查出来deviceCode全是 null,其他字段正常。
  • 原因:MyBatis-Plus 的map-underscore-to-camel-case全局配置没开启,或者 XML 里返回类型没有用resultType而是手写了不完整的resultMap。
  • 解决:确认application.yml里configuration.map-underscore-to-camel-case: true;如果是 XML 查询,优先用resultType而不是手写resultMap。这个现象在自写 SQL 联表查询时尤其常见,多表字段重名时一定要用别名区分。

5.4 登录页样式全丢了:静态资源被拦截

  • 现象:项目启动成功,访问/login页面只剩 HTML,没有任何 CSS/JS 样式。
  • 原因:权限拦截器把静态资源路径也拦了,或者放行规则没写好,导致静态文件请求被重定向到登录接口。
  • 解决:在权限配置里显式放行/css/**、/js/**、/images/**这些静态资源路径;如果有使用 Shiro 或 Spring Security,要区分anon和authc,静态资源用anon放行。这套源码里拦截器用的是自定义 HandlerInterceptor,注意它的excludePathPatterns配置。

5.5 逻辑删除后重复数据:唯一索引与 deleted 的冲突

  • 现象:设备编号唯一索引建在device_code上,用逻辑删除后,删除一台设备再录入相同编号的设备,数据库报唯一索引冲突。
  • 原因:逻辑删除只是把deleted置为 1,device_code和deleted构成唯一约束的话,被删除的记录还占着唯一键的位置。
  • 解决:唯一索引改成(device_code, deleted)联合唯一索引,并在查询时永远带上deleted = 0条件。MyBatis-Plus 的@TableLogic会自动加这个条件,但手写 SQL 时容易漏。这是我见过最多人踩的坑,改表索引后一劳永逸。

6. 进阶技巧:字段自动填充与全局逻辑删除的规范写法

源码能跑通只是第一步,真正让代码好维护的,是几条隐形的全局规范。这套项目里 MyBatis-Plus 的配置价值要比初看高很多,尤其在你需要批量导入历史设备数据时,审计字段和逻辑删除能让数据留痕,而不是硬删。

这里分享一个实用的规范:用 MetaObjectHandler 统一填充创建时间和更新时间。很多初学项目在 Service 层手动setCreateTime(new Date()),每个新增方法都要写一遍,漏写就出现时间为 null 的数据。MyBatis-Plus 提供了注入式填充,只写一次全局生效。

@Component public class AuditMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

配合实体类上的@TableField(fill = FieldFill.INSERT)和@TableField(fill = FieldFill.INSERT_UPDATE),新增设备时不需要手动给createTime赋值,更新设备时也不会忘记刷updateTime。这个组件二开时可以扩展到“当前操作人”字段,只要再注入一个用户上下文就能在每次修改时自动记录操作者,审计链就完整了。

第二个规范是全局逻辑删除的语义。上文 5.5 节提到唯一索引要改成联合唯一,这里补充一点:不要把deleted字段暴露到前端查询条件里。设备列表接口永远只查deleted = 0的数据,如果业务上要查询历史报废设备,走专门的“报废记录”页面,而不是在通用列表里给用户开放一个“包含已删除”的开关。这既保持了数据可追溯,又避免业务方误操作看到一堆已废弃数据。

有了这些规范之后,我的一个习惯是每次新增一张业务表,先问三个问题:主键生成方式是什么?哪些字段需要自动填充?删除要走逻辑删除还是物理删除?这三个问题定了,再开始写实体和建表语句,后面基本不会返工。这套源码读下来,最大的收获不是某个接口怎么写,而是它把状态、审计、删除这三件事做成了全局规则——从那以后我审查代码时,只要发现手写时间戳赋值,都会强制改成自动填充,再让团队成员去理解实体类上那几个注解的意义。

如果你正准备拿这份源码做课程设计或科室二次开发,建议先把第 3 章的状态流转和第 5 章的五个坑过一遍,再决定自己改哪一块。希望这篇拆解能帮你少踩几个真实的业务坑。

本文还有配套的精品资源,点击获取

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

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

立即咨询