ARTICLE DETAIL

资讯详情

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

Spring Boot电子病历系统从零开发:数据库设计、核心功能与避坑指南

Spring Boot电子病历系统从零开发:数据库设计、核心功能与避坑指南 简介这是一套面向毕业设计和课程实训的SpringBoot电子病历管理系统源码包主要服务于Java后端学习者、高校计算机专业学生以及需要快速搭建医疗信息管理Demo的开发者。系统围绕医院电子病历业务展开实现了病历录入、增删改查、就诊记录跟踪、患者在线预约与后台用户/角色权限分配等功能同时涉及数据分析、报告生成、用户认证、数据加密与审计日志等安全设计覆盖医疗信息化系统中的典型需求。压缩包共2011个文件体量49.06MB其中Markdown文档多达1307个适合作为项目说明和开发笔记查阅563个JavaScript文件负责前端交互逻辑95个JSON用于配置与数据交换另有少量HTML、XML等资源辅助页面展示与标准配置。已有69人学习资源内附完整源代码、数据库相关文件及说明文档可直接导入开发环境运行调试也可用于毕设文档撰写、功能扩展或二次开发参考。1. 毕业设计选型Spring Boot 电子病历管理系统到底做什么、值不值得做每年三四月份二手交易区和代码平台上会出现一批长得几乎一模一样的毕业设计条目基于 Spring Boot 的电子病历管理系统源代码 数据库。点进去看绝大多数不是医院级别的大型系统而是一套标准的管理系统工程——患者建档、医生登录、病历书写、处方与检查记录保存、按条件检索病历。选它的人一般有两种需要交一个能完整跑通的毕业设计或者想用一套代码同时练熟 Spring Boot、MyBatis、MySQL 和前端联调。这套系统的难点不在算法和并发而在业务模块怎么切、数据库表怎么建、接口怎么在保存病历和处方时保证数据不丢。落地路径是固定的先建模再搭骨架后写核心接口最后处理部署与前端对接途中会踩到一批非常典型的坑。下面按这个顺序展开中间单独留一章讲踩坑记录。2. 模块划分与数据库设计从就诊流程推导出五张核心表我拿到这类题目的第一件事不是建工程而是先把就诊流程走一遍画出模块边界。电子病历系统最忌讳上来就写一个超大接口把病人、诊断、处方全塞在同一张表里。下面的划分是这类课设最常见的做法也符合大多数验收的复杂度。2.1 业务闭环挂号、写病历、开处方、归档的模块边界一套最小可用的电子病历系统业务闭环是这样的患者挂号建档医生叫号后查看患者基本信息书写病历内容包括主诉、现病史、既往史、初步诊断然后开处方和检查单最后提交并归档。和普通增删改查系统最大的区别在于状态流转一份病历从草稿到已提交再到已归档每个状态下能做的操作不同。草稿可以改、可以删已提交后医生可以撤回归档后不能再改只能追加修订记录。这个状态字段从第一天就要放进表里后面再加会非常痛苦。按这个闭环模块切成四块系统管理账号登录、角色权限、患者管理建档与信息维护、病历管理主诉诊断、状态流转、医嘱与检查处方明细、检查结果回填。对应核心表是五张sys_user、patient、medical_record、prescription、examination。sys_user 是登录账号医生信息挂在 sys_user 的扩展字段里即可patient 只管患者基本资料medical_record 是主表prescription 和 examination 通过 record_id 关联主表。为什么医生不单独建表存密码因为登录体系只认账号一个医生对应一个账号账号表里放 real_name、role 足够。答辩时老师常问「一个医生能不能有多个账号」提前想清楚账号与医生实体怎么解耦回答起来就不慌。2.2 五张核心表建表 SQL、字段类型与关联关系下面是五张表的 MySQL 脚本列类型是我按同类系统常见做法定的直接照抄能跑但有四个关键点值得解释role 用 varchar 而不用 int 枚举因为改角色方便答辩现场不用查字典status 用 tinyint 0/1 表达TEXT 类型用来放主诉和现病史检索不需要对它建索引doctor_id 逻辑关联 sys_user.id不建物理外键原因放在 2.3 里讲。CREATE DATABASE IF NOT EXISTS emr_db DEFAULT CHARSET utf8mb4; USE emr_db; CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(32) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt 密文, real_name VARCHAR(32) DEFAULT NULL COMMENT 真实姓名, role VARCHAR(16) NOT NULL DEFAULT DOCTOR COMMENT 角色: ADMIN/DOCTOR/NURSE, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 启用 0 禁用, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB COMMENT系统账号表; CREATE TABLE patient ( id BIGINT NOT NULL AUTO_INCREMENT, patient_no VARCHAR(32) NOT NULL COMMENT 病历号唯一, name VARCHAR(32) NOT NULL, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0 男 1 女, birth_date DATE DEFAULT NULL COMMENT 出生日期算年龄用, phone VARCHAR(20) DEFAULT NULL, id_card VARCHAR(18) DEFAULT NULL, address VARCHAR(128) DEFAULT NULL, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除 1 已删, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_patient_no (patient_no), KEY idx_patient_name (name) ) ENGINEInnoDB COMMENT患者表; CREATE TABLE medical_record ( id BIGINT NOT NULL AUTO_INCREMENT, record_no VARCHAR(32) NOT NULL COMMENT 病历编号, patient_id BIGINT NOT NULL COMMENT 关联 patient.id, doctor_id BIGINT NOT NULL COMMENT 关联 sys_user.id, chief_complaint TEXT COMMENT 主诉, present_illness TEXT COMMENT 现病史, past_history TEXT COMMENT 既往史, diagnosis TEXT COMMENT 初步诊断, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 草稿 1 已提交 2 已归档, visit_date DATE NOT NULL COMMENT 就诊日期, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_patient_visit (patient_id, visit_date) ) ENGINEInnoDB COMMENT病历主表; CREATE TABLE prescription ( id BIGINT NOT NULL AUTO_INCREMENT, record_id BIGINT NOT NULL COMMENT 关联 medical_record.id, drug_name VARCHAR(64) NOT NULL COMMENT 药品名, dosage VARCHAR(64) DEFAULT NULL COMMENT 单次剂量, frequency VARCHAR(32) DEFAULT NULL COMMENT 用药频率, days INT DEFAULT NULL COMMENT 用药天数, amount DECIMAL(10,2) DEFAULT NULL COMMENT 金额, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_prescription_record (record_id) ) ENGINEInnoDB COMMENT处方明细表; CREATE TABLE examination ( id BIGINT NOT NULL AUTO_INCREMENT, record_id BIGINT NOT NULL COMMENT 关联 medical_record.id, item_name VARCHAR(64) NOT NULL COMMENT 检查项目, result_value VARCHAR(255) DEFAULT NULL COMMENT 结果值, result_unit VARCHAR(16) DEFAULT NULL COMMENT 单位, reference_range VARCHAR(32) DEFAULT NULL COMMENT 参考范围, result_status TINYINT NOT NULL DEFAULT 0 COMMENT 0 未出 1 正常 2 异常, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_exam_record (record_id) ) ENGINEInnoDB COMMENT检查检验表;主表与明细表的关联通过 record_id 完成一套病历可以带多条处方和多条检查记录。字段上最容易被忽略的是 medical_record 的 status它决定了医生端显示哪些按钮草稿显示「编辑/提交」已提交显示「撤回/归档」已归档只读。下面这张表是选型速查表写论文时可以直接搬。表关键字段为什么不能省sys_userusername 唯一键、password 密文、role登录鉴权和医生身份都靠它patientpatient_no 唯一、birth_date 用于算年龄不存 age年龄会随时间变化medical_recordstatus、visit_date、chief_complaint状态决定业务流转prescriptionrecord_id、drug_name、dosage、amount一对多明细金额要定点数examinationrecord_id、item_name、result_status检查结果要标记正常/异常2.3 主键、逻辑删除与时间字段三个影响全表设计的选择主键方案直接说结论单机课设用自增 BIGINT别为了看起来企业级就上雪花 ID。雪花 ID 返回给前端时会出现 JS 精度丢失16 位以上的 Long 被截断成浮点数最后还要额外配置 Long 转 String 的序列化器绕一圈。单库单表没有分库分表诉求自增主键走聚簇索引插入查询都省心。如果论文里想写分布式主键把它放在「后续扩展」一节更稳妥写进实现只会给自己添麻烦。逻辑删除不是可选项。病历数据不能物理删医生误操作删除也要留痕这是电子病历的基本要求。做法是给 patient 表加 deleted tinyint默认 0删除操作只改成 1。MyBatis-Plus 里可以给实体字段标 TableLogic之后它的 select 会自动追加 deleted0update 和 delete 也会自动带条件省去手动拼 SQL。要注意的是逻辑删除字段只放在 patient 上病历主表一般不做删除操作只做状态流转。时间字段统一用 datetime 加数据库默认值。timestamp 有 2038 年问题且受时区影响datetime 直白好排查。created_time 用 DEFAULT CURRENT_TIMESTAMPupdated_time 加 ON UPDATE CURRENT_TIMESTAMP让数据库自己维护应用层不要传值。另外JDBC 连接串里必须带 serverTimezoneAsia/Shanghai不然后面会出现差 8 小时的问题这个坑在第 5 章单独展开。提示建表时 CHARSET 用 utf8mb4不要用 utf8。MySQL 里的 utf8 是 utf8mb3 的别名存 emoji 或生僻字会直接报 Incorrect string value。3. 搭建 Spring Boot 骨架依赖版本、目录结构与核心配置模型定完开始搭工程。这一步看起来简单但版本选择会决定你后面哭不哭。每年都有同学拿着装好的 JDK8 去跑 Spring Boot 3启动失败才回头问为什么。3.1 Maven 依赖与版本选择JDK 版本、Spring Boot 版本和 MyBatis-Plus 适配先说结论实验室机器是 JDK8就用 Spring Boot 2.7.x推荐 2.7.18已经是 JDK17 且想用新特性可以选 3.x但要接受两个改动——javax 包名全部变成 jakartaMyBatis-Plus 必须用 3.5.3 以上的适配版本否则很多旧教程的写法编译不过。很多人遇到的 springboot版本太高导致的启动失败绝大多数是 JDK 和依赖版本不匹配造成的。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency /dependenciesMyBatis-Plus 3.5.x 比较好用自带分页插件、逻辑删除和 LambdaQueryWrapper省掉写 XML 的时间。mysql-connector-j 8.0.33 同时兼容 MySQL 5.7 和 8.0学校机房用哪个版本都不怕。jjwt 是 JWT 的标准库分开三个坐标是因为 jjwt-api 是编译期依赖jjwt-impl 只在运行时需要。3.2 项目目录结构controller / service / mapper / entity 四层怎么分Spring Boot 项目结构本身不复杂但分层的职责要明确。拿到一套源代码我一般先看目录目录乱的项目后面一定乱。下面是我自己惯用的分包方式src/main/java/com/example/emr/ ├── EmrApplication.java ├── common/ │ ├── Result.java // 统一返回体 │ ├── GlobalExceptionHandler.java // 全局异常处理 │ └── JwtUtil.java // JWT 工具 ├── config/ │ ├── WebMvcConfig.java // 拦截器、静态资源映射 │ └── MybatisPlusConfig.java // 分页插件 ├── controller/ │ ├── AuthController.java │ ├── PatientController.java │ └── MedicalRecordController.java ├── service/ │ ├── PatientService.java │ ├── MedicalRecordService.java │ └── impl/ ├── mapper/ │ ├── PatientMapper.java │ └── MedicalRecordMapper.java ├── entity/ │ ├── Patient.java │ ├── MedicalRecord.java │ └── SysUser.java └── dto/ ├── LoginDTO.java └── MedicalRecordSaveDTO.javacontroller 只接收参数把请求数据转换成 DTO然后调 service不写业务service 是事务边界所在所有跨表操作都从 service 方法进入mapper 继承 MyBatis-Plus 的 BaseMapper必要时在 XML 里写动态 SQLentity 与数据库表一一对应字段名走驼峰转下划线dto 承接前端入参避免二十个字段的实体直接裸露给接口。注意主类 EmrApplication 要放在根包 com.example.emr 下SpringBootApplication 默认只扫描根包下的 controller、service、mapper放错位置接口会全部 404。3.3 application.yml 核心配置端口、数据源、连接池与上传限制配置是 Spring Boot 里最容易出玄学问题的地方其实核心就这几个。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/emr_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: ${DB_PASSWORD:123456} hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0数据源 URL 上三个参数缺一不可useSSLfalse 关掉证书警告serverTimezoneAsia/Shanghai 修正时区characterEncodingutf8 防中文乱码。Hikari 是 Spring Boot 默认连接池maximum-pool-size 10 对课设单机绰绰有余别设成 1并发一高就卡死也别设成 100MySQL 默认最大连接数是 151池子再大也没用。multipart 不配置的话默认单文件 1MB上传病历附件一多直接报错。map-underscore-to-camel-case 必须开否则数据库列 created_time 映射不到实体属性 createdTime这个坑在避坑章是头号选手。全局异常处理器用 RestControllerAdvice 把 RuntimeException 统一转成 Result 结构controller 里就不用到处 try-catch 了。注意配置里的数据库密码不要以明文提交到开源平台。用 ${DB_PASSWORD:123456} 这种占位符本地默认值留给开发部署时用环境变量覆盖。4. 核心功能落地登录鉴权、病历增删改查和条件分页检索骨架跑通后核心功能就三类登录与权限、病历的增删改查、分页检索。这三块能串起来答辩演示就有底气了。4.1 登录与角色鉴权BCrypt 密码校验加 JWT 签发的最小实现登录接口不复杂但密码校验方式很关键。不用 MD5 加盐直接用 BCrypt因为 BCrypt 把盐拼在密文里校验时自动使用不需要额外存盐。RestController RequestMapping(/api/auth) public class AuthController { Autowired private SysUserService sysUserService; PostMapping(/login) public Result login(RequestBody Valid LoginDTO dto) { // 1. 按用户名查账号 SysUser user sysUserService.findByUsername(dto.getUsername()); // 2. BCrypt 校验明文和密文 if (user null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error(账号或密码错误); } if (user.getStatus() ! 1) { return Result.error(账号已禁用); } // 3. 签发 JWT把 userId 和 role 放进去 String token JwtUtil.createToken(user.getId(), user.getRole()); return Result.ok().put(token, token).put(realName, user.getRealName()); } }JwtUtil 里最需要注意的是过期时间参数单位是毫秒。public static String createToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); }2 * 60 * 60 * 1000 是两小时如果你写成 2 * 60 * 60得到的是两分钟有效期登录后写几个接口就掉线这就是典型的玄学掉线问题。SECRET 字符串要 32 字节以上HS256 算法密钥太短会抛 WeakKeyException。JWT 无状态后端不存 sessionVue 和移动端都好接前端拿到 token 后放在 Authorization 头里后端拦截器解析出来塞进 ThreadLocal 即可不用每次查数据库确认登录状态。4.2 病历的增删改查事务保证主表和明细一起入库病历保存是这套系统里最值得写清楚的方法因为跨了两张表主表 medical_record 和明细表 prescription。增删改查这四个字看起来常规真正的坑在事务边界。Service public class MedicalRecordServiceImpl extends ServiceImplMedicalRecordMapper, MedicalRecord implements MedicalRecordService { Autowired private PrescriptionMapper prescriptionMapper; Override Transactional(rollbackFor Exception.class) public Long saveRecordWithPrescription(MedicalRecordSaveDTO dto) { // 1. 先保存主表拿到自增 ID MedicalRecord record new MedicalRecord(); record.setPatientId(dto.getPatientId()); record.setChiefComplaint(dto.getChiefComplaint()); record.setPresentIllness(dto.getPresentIllness()); record.setDiagnosis(dto.getDiagnosis()); record.setStatus(0); // 草稿状态 record.setVisitDate(LocalDate.now()); this.save(record); // 2. 循环保存处方明细 if (dto.getPrescriptions() ! null) { for (PrescriptionDTO p : dto.getPrescriptions()) { Prescription item new Prescription(); item.setRecordId(record.getId()); item.setDrugName(p.getDrugName()); item.setDosage(p.getDosage()); item.setFrequency(p.getFrequency()); item.setAmount(p.getAmount()); prescriptionMapper.insert(item); } } return record.getId(); } }Transactional 放在 service 方法上不放在 controller因为 Spring 事务基于代理controller 里的事务不生效。rollbackFor 写成 Exception.class 很重要默认配置只回滚 RuntimeException如果你在代码里手动throw new Exception(保存失败)事务不会回滚这就是「明明抛了异常数据还是写进去了」的原因。先存主表再循环插明细顺序不能反拿到自增主键后子表才有外键值。如果将来数据量大循环 insert 可以换成批量 insert但毕业设计这个量级完全不用优化。4.3 条件分页检索分页插件配置与按时间、状态过滤分页插件是 MyBatis-Plus 3.5.x 的标准用法先注册拦截器。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor page new PaginationInnerInterceptor(DbType.MYSQL); page.setMaxLimit(100L); // 单页最多 100 条 page.setOverflow(false); // 超过总页数返回空不自动回折 interceptor.addInnerInterceptor(page); return interceptor; } }查询方法用 LambdaQueryWrapper 拼条件比字符串 SQL 安全字段名写错在编译期就能发现。public PageMedicalRecord search(MedicalRecordQueryDTO query) { LambdaQueryWrapperMedicalRecord wrapper new LambdaQueryWrapper(); wrapper.eq(query.getStatus() ! null, MedicalRecord::getStatus, query.getStatus()) .between(query.getStart() ! null query.getEnd() ! null, MedicalRecord::getVisitDate, query.getStart(), query.getEnd()) .orderByDesc(MedicalRecord::getCreatedTime); return this.page(new Page(query.getPageNum(), query.getPageSize()), wrapper); }pageNum 要从 1 开始传。MyBatis-Plus 的 Page 计算 offset 时用的是(current - 1) * size前端如果从 0 开始传生成的 SQL 会是limit -10,10结果不对还很难排查。maxLimit 设 100 是为了防止有人把 pageSize 传成 99999 直接拖垮数据库。分页插件只认 MyBatis-Plus 自己的查询方法别和 PageHelper 的 startPage 混用两种分页叠加会生成奇怪的 SQL选一种用到底。如果检索条件里要按患者姓名模糊查就得 join patient 表这种动态条件的 join 建议写 XML用if标签拼比 QueryWrapper 直观得多。5. 避坑与排查我在这类系统上踩过的 5 个坑做这套系统时很多报错看起来毫无头绪最后发现原因都很基础。下面五条按「现象→原因→解决」写清楚遇到时直接对号入座。5.1 接口返 500日志出现 created_time 找不到列现象登录和分页接口都正常查询病历详情时后端 500日志里是 BadSqlGrammarException。原因MyBatis 把数据库列 created_time 映射到实体属性 createdTime 失败。实体属性是驼峰命名数据库列是下划线命名但 yml 里没有开启 map-underscore-to-camel-case。解决在 application.yml 的 mybatis-plus.configuration 下加 map-underscore-to-camel-case: true。另一个更隐蔽的原因是实体字段与数据库列名真不一致比如数据库列是 record_no实体里写了 recordNo但没加 TableField(record_no)MyBatis-Plus 在 insert 时会把属性名转成 recordno 当作列名直接语法错误。最稳的做法是给所有实体字段显式标 TableField哪怕字段名完全一致。5.2 JWT 登录后不到 5 分钟就失效现象登录成功写几个接口再操作提示 token 过期。原因setExpiration 接收的是 Date很多人直接写System.currentTimeMillis() 30这在代码里看起来像 30 分钟实际是 30 毫秒。这种错很难靠肉眼发现因为人的预期会自动把 30 当作 30 分钟。解决显式写成 30 * 60 * 1000 并加注释表示 30 分钟。开发期可以设成 12 小时答辩演示别设太短不然演示到一半要重新登录很尴尬。另一个相关坑是 secret 太短抛 WeakKeyExceptionHS256 要求密钥至少 32 字节。5.3 页面时间比数据库差 8 小时现象开发机一切正常部署到服务器后列表时间全是 UTC比数据库实际时间晚 8 小时。原因JDBC URL 没带 serverTimezoneAsia/Shanghai服务器默认时区是 UTC或者 Jackson 在反序列化 LocalDateTime 时也按 UTC 处理了。解决URL 参数加上 serverTimezoneAsia/Shanghai同时在 application.yml 里配 spring.jackson.time-zone: GMT8。两个地方都要配只改一个仍然可能差 8 小时。前端如果也差 8 小时检查 axios 拦截器有没有对后端返回的时间字符串做了多余的 new Date 转换。5.4 Vue 打包放进 Spring Boot 后刷新二级路由 404现象打包后首页能打开从 /record/1 刷新页面Spring Boot 返回 404。原因Vue history 路由把路径交给浏览器地址栏后端没有对应的 controllerSpring Boot 静态资源映射只处理根路径的 index.html二级路径没有对应文件。解决最简单的是把前端路由改成 hash 模式访问地址变成 /#/record/1后端完全不用处理。要保留 history 模式就在后端写一个转发 controller把没有匹配到的路径 forward 到 /index.html。课设阶段我建议直接用 hash 模式少一个坑论文里可以写「为保证部署简单前端采用 hash 路由」。5.5 Transactional 好像失效数据仍然写进去了现象保存病历的 service 里主动抛异常前端却看到处方明细插入成功了主表也有数据。原因同一个类里 A 方法调用 B 方法B 上的 Transactional 不生效。Spring 事务用代理实现自调用时走的是原始对象而不是代理对象事务注解被直接跳过。解决把事务方法放到另一个 service 类里或者注入自身的代理对象再调用。同时把 rollbackFor 写成 Exception.class因为默认只回滚 RuntimeException手动 new Exception 抛出去不会触发回滚。自调用失效是 Spring Boot 面试题里的高频考点答辩时被老师问到概率不低把这个坑讲清楚比多写一个功能更能加分。6. 进阶技巧把接口文档化用一条脚本贯穿整套流程到能跑通的阶段离验收还差一步让答辩老师能在最短时间内看懂整个系统。我的习惯是把接口暴露成 swagger-ui再写一条自测脚本演示现场跑一遍。这样不用挨个接口去点也不会出现演示时找不到参数的尴尬。6.1 用 springdoc-openapi 生成可点击接口文档加一个依赖启动后浏览器访问 /swagger-ui/index.html就能看到所有接口的入参和返回结构。dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-ui/artifactId version1.7.0/version /dependencycontroller 路径统一带 /apitoken 在页面右上角 Authorize 里填 Bearer 值之后每个接口都能直接点击调试。swagger 是开发期联调用的上线前可以关掉避免接口结构暴露。6.2 自测脚本登录、建档、写病历、分页查询一条龙#!/bin/bash BASEhttp://localhost:8080/api # 1. 登录拿 token TOKEN$(curl -s $BASE/auth/login -H Content-Type: application/json \ -d {username:admin,password:123456} | python3 -c import sys,json;print(json.load(sys.stdin)[data][token])) # 2. 给患者建档返回患者 ID PATIENT_ID$(curl -s $BASE/patient -H Authorization: Bearer $TOKEN -H Content-Type: application/json \ -d {name:张三,gender:0,birthDate:2000-01-01,phone:13800000001} \ | python3 -c import sys,json;print(json.load(sys.stdin)[data])) # 3. 写一份带处方明细的病历 curl -s $BASE/medical-record -H Authorization: Bearer $TOKEN -H Content-Type: application/json \ -d {\patientId\:$PATIENT_ID,\chiefComplaint\:\咳嗽三天\,\prescriptions\:[{\drugName\:\阿莫西林\,\dosage\:\0.5g\,\frequency\:\一日三次\}]} # 4. 分页查询病历列表 curl -s $BASE/medical-record/page?pageNum1pageSize10 -H Authorization: Bearer $TOKEN脚本用 python3 解析 JSON机器没装 python 就换成 jq。JSON 字符串里的双引号在 bash 里要转义注意别漏。这四条命令把「登录→建档→写病历→查询」整条链路走完比我当年手动点接口靠谱太多。我最早做这类系统时答辩前觉得接口都通现场却在 IDEA 里开着旧进程导致端口占用白屏了半分钟。后来养成习惯任何演示前先 maven 打包再用这条脚本从头到尾跑一遍。接口文档化加自测脚本这两个习惯比多写一个功能更省心希望帮到你。本文还有配套的精品资源点击获取
返回列表