☰
Spring Boot电子病历系统从零开发:数据库设计、核心功能与避坑指南
2026/10/7 16:43:19 网站建设 项目流程

简介:这是一套面向毕业设计和课程实训的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) ) ENGINE=InnoDB 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) ) ENGINE=InnoDB 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) ) ENGINE=InnoDB 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) ) ENGINE=InnoDB 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) ) ENGINE=InnoDB 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 会自动追加 deleted=0,update 和 delete 也会自动带条件,省去手动拼 SQL。要注意的是逻辑删除字段只放在 patient 上,病历主表一般不做删除操作,只做状态流转。

时间字段统一用 datetime 加数据库默认值。timestamp 有 2038 年问题且受时区影响,datetime 直白好排查。created_time 用 DEFAULT CURRENT_TIMESTAMP,updated_time 加 ON UPDATE CURRENT_TIMESTAMP,让数据库自己维护,应用层不要传值。另外,JDBC 连接串里必须带 serverTimezone=Asia/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 包名全部变成 jakarta,MyBatis-Plus 必须用 3.5.3 以上的适配版本,否则很多旧教程的写法编译不过。很多人遇到的 springboot版本太高导致的启动失败,绝大多数是 JDK 和依赖版本不匹配造成的。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <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.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> </dependencies>

MyBatis-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.java

controller 只接收参数,把请求数据转换成 DTO,然后调 service,不写业务;service 是事务边界所在,所有跨表操作都从 service 方法进入;mapper 继承 MyBatis-Plus 的 BaseMapper,必要时在 XML 里写动态 SQL;entity 与数据库表一一对应,字段名走驼峰转下划线;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?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 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 上三个参数缺一不可:useSSL=false 关掉证书警告;serverTimezone=Asia/Shanghai 修正时区;characterEncoding=utf8 防中文乱码。Hikari 是 Spring Boot 默认连接池,maximum-pool-size 10 对课设单机绰绰有余,别设成 1,并发一高就卡死,也别设成 100,MySQL 默认最大连接数是 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 无状态,后端不存 session,Vue 和移动端都好接,前端拿到 token 后放在 Authorization 头里,后端拦截器解析出来塞进 ThreadLocal 即可,不用每次查数据库确认登录状态。

4.2 病历的增删改查:事务保证主表和明细一起入库

病历保存是这套系统里最值得写清楚的方法,因为跨了两张表:主表 medical_record 和明细表 prescription。增删改查这四个字看起来常规,真正的坑在事务边界。

@Service public class MedicalRecordServiceImpl extends ServiceImpl<MedicalRecordMapper, 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 Page<MedicalRecord> search(MedicalRecordQueryDTO query) { LambdaQueryWrapper<MedicalRecord> 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 太短抛 WeakKeyException,HS256 要求密钥至少 32 字节。

5.3 页面时间比数据库差 8 小时

现象:开发机一切正常,部署到服务器后列表时间全是 UTC,比数据库实际时间晚 8 小时。

原因:JDBC URL 没带 serverTimezone=Asia/Shanghai,服务器默认时区是 UTC;或者 Jackson 在反序列化 LocalDateTime 时也按 UTC 处理了。

解决:URL 参数加上 serverTimezone=Asia/Shanghai,同时在 application.yml 里配 spring.jackson.time-zone: GMT+8。两个地方都要配,只改一个仍然可能差 8 小时。前端如果也差 8 小时,检查 axios 拦截器有没有对后端返回的时间字符串做了多余的 new Date 转换。

5.4 Vue 打包放进 Spring Boot 后,刷新二级路由 404

现象:打包后首页能打开,从 /record/1 刷新页面,Spring Boot 返回 404。

原因:Vue history 路由把路径交给浏览器地址栏,后端没有对应的 controller;Spring 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> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.7.0</version> </dependency>

controller 路径统一带 /api,token 在页面右上角 Authorize 里填 Bearer 值,之后每个接口都能直接点击调试。swagger 是开发期联调用的,上线前可以关掉,避免接口结构暴露。

6.2 自测脚本:登录、建档、写病历、分页查询一条龙

#!/bin/bash BASE=http://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?pageNum=1&pageSize=10" -H "Authorization: Bearer $TOKEN"

脚本用 python3 解析 JSON,机器没装 python 就换成 jq。JSON 字符串里的双引号在 bash 里要转义,注意别漏。这四条命令把「登录→建档→写病历→查询」整条链路走完,比我当年手动点接口靠谱太多。我最早做这类系统时,答辩前觉得接口都通,现场却在 IDEA 里开着旧进程导致端口占用,白屏了半分钟。后来养成习惯:任何演示前先 maven 打包,再用这条脚本从头到尾跑一遍。接口文档化加自测脚本这两个习惯,比多写一个功能更省心,希望帮到你。

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

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

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

立即咨询