拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于SpringBoot3+Vue3的电子病历系统毕业设计:从建模到部署全指南

简介面向2025届计算机与软件工程专业毕业设计及课程设计这套电子病历系统以Java为根基前端采用Vue.js3构建管理后台与用户前台后端基于SpringBoot3配合MySQL8数据库实现医疗信息存储、管理、传输与重现等核心功能既贴合医院信息化平台建设需求也适合开发者系统理解前后端分离架构。资源共含6个文件压缩包合计约75.83MB覆盖3个zip完整源码、返修源码及返修文档、1个SQL数据库脚本、需求文档截图与系统操作演示MP4按源码、数据库、文档、录屏分层组织便于对照学习与部署。目前已有78人学习下载作为可直接运行的毕设项目具备一定参考热度。借助启动教程与录屏可快速完成环境搭建并复现功能对照初版与返修源码、返修文档能够梳理开发与修改脉络结合SQL脚本和需求文档掌握MySQL8表设计、SpringBoot3接口开发及Vue.js3组件化构建的完整实践对提升综合开发能力有明显帮助。1. 电子病历系统的毕业设计选题为什么 2025 年依然值得做电子病历系统EMRElectronic Medical Record是医疗信息化的核心子系统它解决的不是「把 Word 文档存进数据库」这种表层问题而是把门诊、住院、医嘱、检验检查、病案归档这些分散流程收敛到一条可追踪、可检索、可质控的数据链路上。2025 年的毕业设计选这个题目技术栈恰好落在 JAVASpringBoot3Vue.js3 上意味着你要同时处理传统业务系统的事务一致性、权限模型、复杂表单以及现代前端的状态管理和组件化设计——这比单纯做个 CRUD 管理系统更能体现工程能力。从答辩和查重的角度看电子病历系统有天然的「业务复杂度」。病历不是一张表能装下的它涉及患者主索引、就诊记录、病历文书包含主诉、现病史、体格检查、诊断、治疗意见等结构化字段、医嘱、检验报告关联还有医生的签名和时间戳留痕。只要把这几块关系理清楚论文里写「需求分析」「数据库设计」「模块实现」每章都有实打实的内容。适合对 Java 后端、关系型数据库、Vue 前端都有一定基础想通过一个完整项目把 SpringBoot3 和 Vue3 串起来的中高年级学生。本文接下来会从基本概念和技术选型入手逐步讲清楚如何设计数据模型、如何实现前后端通信、如何把权限和病历留痕做扎实最后给出部署和排障的建议。内容以可运行、可复现的代码为主范围控制在 SpringBoot3 Vue3 MySQL或 PostgreSQL这一套主流组合内不涉及任何外部代理或网络工具。2. 先把基础打牢SpringBoot3 与 Vue3 在电子病历场景下的选型和技术特点2.1 为什么是 SpringBoot3 而不是 SpringBoot2 或 SSMSpringBoot3 的基础是 Spring Framework 6 和 Jakarta EE 9这意味着它默认使用 Jakarta 命名空间jakarta.servlet.*而不是javax.servlet.*并且最低要求 JDK 17。对 2025 年的毕业设计来说选 SpringBoot3 的好处是起步依赖更清晰配置更少内置的spring-boot-starter-validation、spring-boot-starter-security都跟随了最新的稳定 API。如果你是先从 SSMSpring SpringMVC MyBatis学过来的切换到 SpringBoot3 后最直观的感受是省掉了大量 XML 配置RestControllerServiceMapper这套分层写法可以原样保留。在电子病历系统里SpringBoot3 最适合承载的是「业务规则引擎」。比如病历的必填项校验、诊断编码ICD-10的格式校验、医嘱的重复检查、病历状态的流转控制这些逻辑放在 Service 层用 Java 写比放在数据库存储过程里更易读、易测试。同时 SpringBoot3 对Transactional的支持没有退化病历保存时往往要同时写主表和明细表事务边界仍然是用注解声明最方便。一个要注意的适配点SpringBoot3 默认使用springdoc-openapi进行接口文档生成时需要选择 2.x 版本才兼容 JakartaMyBatis 也需要使用mybatis-spring-boot-starter的 3.0 以上版本否则启动时可能会报ClassNotFoundException: javax.sql.DataSource。这些版本兼容性问题在 2025 年虽然已有稳定方案但如果你是从旧教程里复制的依赖很容易踩坑。2.2 Vue3 在电子病历前端中的角色组合式 API 与组件化Vue3 的 Composition API组合式 API非常适合电子病历这种「一个页面里有多种复杂交互」的场景。以病历编辑页为例页面上同时存在患者基本信息、主诉输入框、诊断下拉选择、医嘱表格、检验检查结果展示这些区域各自有自己的状态又要共享同一个「当前病历」对象。用 Options API 写容易把逻辑堆在data和methods里而用组合式 API可以把「病历加载」「校验」「自动保存」拆成独立的 composable 函数每个函数只关心一块逻辑。前端组件化方面我建议至少拆分这样几个组件患者信息卡、病历表单、诊断列表、医嘱录入表格、病历历史时间线。每个组件接收明确的 props通过emit或 Pinia 完成跨组件通信。Electron 病历系统的「暂存」功能很关键——医生可能在编辑过程中被叫走所以要有草稿保存机制这个用 Vue3 的watch配合定时器实现很顺手数据变化后自动调用保存接口避免丢失。这里还要说清楚一个对比如果你用 Vue2 Element UI 做组件生态成熟但组合式 API 不如 Vue3 灵活用 Vue3 Element Plus表单校验、表格编辑、弹窗确认这些医疗系统高频需求都有现成方案但要注意 Element Plus 对 Vue3.4 之后的版本适配已经稳定不必担心。真正的难点不在框架本身而在「结构化病历」的数据组织——Vue3 只是让你表达这种组织更舒服。2.3 数据库选择MySQL 还是 PostgreSQL以及为什么不需要 NoSQL电子病历系统对数据一致性要求很高不建议引入 MongoDB 或 Elasticsearch 作为主要存储。NoSQL 适合存「非结构化日志」但病历的核心是「患者—就诊—文书—医嘱」这样的强关联数据用关系型数据库建模后查某患者所有历史病历只需一条 SQL如果丢到文档数据库里反而要自己维护关联关系。MySQL 8.0 是默认选型因为环境普遍、资料多、排错方便如果你熟悉 PostgreSQL它支持 JSONB 类型可以让「检验报告明细」这类半结构化数据直接存成 JSON穿插在关系表里也算一个合理改良。具体到表设计后面第 4 章会给出核心建表语句。这里强调一个观念电子病历系统不要设计成「一张大宽表」。病历文书虽然看起来像是一整篇文本但它包含的结构化字段——主诉、现病史、既往史、体格检查、初步诊断——在质控和检索时往往要单独过滤。建议把「病历主表」和「病历明细表」分开主表存patient_id、visit_id、doctor_id、status、create_time明细表一行存一个字段名和字段值。这样新增一个结构化字段时不需要改表结构只需要在前端渲染时配置字段元数据。3. 系统设计与数据库建模把病历拆成可存储、可检索的关系结构3.1 核心实体关系患者、就诊、病历、医嘱的四层模型实体核心字段与其他实体的关系说明patient患者id、name、id_card、phone、birthday1 对 N 关联 visit患者主索引身份证号做唯一约束visit就诊id、patient_id、visit_type、visit_time、department_idN 对 1 关联 patient1 对 N 关联 medical_record一次就诊对应一份主病历medical_record病历id、visit_id、doctor_id、status、content_jsonN 对 1 关联 visit1 对 N 关联 record_item主表存状态明细表存结构化字段record_item病历明细record_id、field_key、field_value、sort_orderN 对 1 关联 medical_record一行一个病史字段prescription医嘱id、visit_id、drug_name、dosage、frequency、statusN 对 1 关联 visit与病历同属一次就诊sys_user用户id、username、password、real_name、role与 patient/visit 无直接外键医生、护士、管理员统一认证这张表的关键在于medical_record和record_item的拆法。有些教程会把整个病历存成一个content大文本字段前端提交什么就存什么。这样做开发快但有几个问题第一无法在数据库层做字段级校验第二做病历质控时想统计「有多少病历主诉为空」只能用 LIKE 查询效率低且不准确。所以我建议至少对高频字段做结构化存储低频的自定义字段退回到 JSON 字段。MySQL 8 的JSON类型可以承担这部分数据但注意不要在 JSON 里放需要频繁关联查询的数据。3.2 建表语句直接能跑的 MySQL DDL 与关键索引设计以下是核心表的建表 SQL精简了部分字段保留最能说明设计思路的部分。-- 患者表 CREATE TABLE patient ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 姓名, id_card VARCHAR(18) NOT NULL UNIQUE COMMENT 身份证号, phone VARCHAR(20) DEFAULT COMMENT 联系电话, birthday DATE DEFAULT NULL COMMENT 出生日期, gender TINYINT DEFAULT 2 COMMENT 0未知 1男 2女, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT患者主索引; -- 就诊表 CREATE TABLE visit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, visit_type TINYINT NOT NULL DEFAULT 1 COMMENT 1门诊 2住院 3急诊, visit_time DATETIME NOT NULL COMMENT 就诊时间, department_id BIGINT NOT NULL COMMENT 科室ID, status TINYINT DEFAULT 1 COMMENT 1在诊 2完成 3已作废, KEY idx_patient_visit_time (patient_id, visit_time), CONSTRAINT fk_visit_patient FOREIGN KEY (patient_id) REFERENCES patient (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT就诊记录; -- 病历主表 CREATE TABLE medical_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, visit_id BIGINT NOT NULL, doctor_id BIGINT NOT NULL COMMENT 经治医生, chief_complaint TEXT COMMENT 主诉, present_illness TEXT COMMENT 现病史, past_history TEXT COMMENT 既往史, physical_exam TEXT COMMENT 体格检查, initial_diagnosis VARCHAR(512) COMMENT 初步诊断ICD-10编码, status TINYINT DEFAULT 0 COMMENT 0草稿 1已暂存 2已提交 3已归档, submit_time DATETIME DEFAULT NULL COMMENT 正式提交时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_visit_status (visit_id, status), CONSTRAINT fk_record_visit FOREIGN KEY (visit_id) REFERENCES visit (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT病历主记录; -- 医嘱表 CREATE TABLE prescription ( id BIGINT PRIMARY KEY AUTO_INCREMENT, visit_id BIGINT NOT NULL, drug_name VARCHAR(128) NOT NULL COMMENT 药品名称, dosage VARCHAR(64) DEFAULT NULL COMMENT 单次用量, frequency VARCHAR(32) DEFAULT NULL COMMENT 频次 qd/bid/tid, days INT DEFAULT NULL COMMENT 用药天数, status TINYINT DEFAULT 1 COMMENT 1有效 0已停用, KEY idx_prescription_visit (visit_id), CONSTRAINT fk_prescription_visit FOREIGN KEY (visit_id) REFERENCES visit (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医嘱信息;这里说明几个容易被忽视的细节。visit表上的联合索引(patient_id, visit_time)是为了支撑「查某患者最近的就诊记录」这种高频查询比单独在patient_id建索引效果更好因为查询通常会带时间范围排序。medical_record上的(visit_id, status)索引同理支撑「某次就诊下是否存在已提交病历」的判断。外键在这个项目中保留是合理的因为毕业设计场景下数据量不大牺牲一点写入性能换取约束完整性更划算如果你的导师强调大数据量下的性能可以在论文里说明「生产环境可能去除外键改用应用层校验」这也是一个可以展开论述的点。3.3 病历状态流转草稿、暂存、提交、归档的状态机设计病历不能一次性写完所以状态流转是系统里最容易被问到、也最能体现设计能力的环节。建议定义为四个状态0草稿DRAFT、1暂存SAVED、2已提交SUBMITTED、3已归档ARCHIVED。流转规则如下新开病历时直接创建草稿此时doctor_id和visit_id已确定但内容可以为空。医生点击「暂存」时状态从0变为1只保存内容不触发必填校验也不生成提交时间。医生点击「提交」时状态从1变为2触发所有必填校验主诉、现病史、诊断至少一项不能为空通过后写入submit_time。已提交的病历不允许直接修改只能走「申请修改」流程由护士或上级医生驳回后状态退回1。归档状态3是出院或诊后最终锁定只读。这一步在代码里不要写成一堆散落的if/else建议用状态机枚举来管理。后面第 4 章的 Service 代码会给出具体实现这里先把规则定清楚数据库表里status注释和代码枚举保持一致避免前后端各说各话。4. SpringBoot3 后端实现从 DTO 校验到病历保存的事务边界4.1 创建 SpringBoot3 项目并配置依赖如果你用 IDEA 创建 SpringBoot3 项目要注意选择 Spring Initializr 中的 Spring Boot 3.2.x 或更新版本JDK 选 17 或 21。pom.xml里建议加入以下依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency逻辑说明spring-boot-starter-web提供 REST 接口能力validation用于 DTO 字段校验避免每个接口手写参数判断security用于登录认证和接口权限控制毕业设计很难绕开MyBatis-Plus 是 MyBatis 的增强工具提供BaseMapper和IService能让简单的 CRUD 少写一半代码。如果你的导师要求「体现原生 MyBatis 能力」可以不用 MyBatis-Plus改用Insert、Select注解直接在 Mapper 上写 SQL但工作量会明显上升。注意mybatis-plus-boot-starter3.5.5 以上的版本才完整支持 SpringBoot3早期 3.5.3 及之前的版本可能报Failed to process import candidates。遇到这个问题不要慌把版本换成 3.5.5 或点击 IDEA 的 Maven 面板 Reload 重新加载。4.2 病历保存接口事务、状态机与字段校验病历保存是核心接口设计为POST /api/medical-records/save。请求体 DTO 包含病历主表字段和医嘱列表一次提交同时落两张表。以下是 Service 层的核心代码Service public class MedicalRecordServiceImpl extends ServiceImplMedicalRecordMapper, MedicalRecord implements MedicalRecordService { Autowired private PrescriptionMapper prescriptionMapper; Override Transactional(rollbackFor Exception.class) public Long saveMedicalRecord(MedicalRecordSaveDTO dto) { // 1. 查询原病历判断当前状态是否允许修改 MedicalRecord record this.getById(dto.getId()); if (record null) { record new MedicalRecord(); record.setVisitId(dto.getVisitId()); record.setDoctorId(SecurityUtils.getCurrentUserId()); record.setStatus(MedicalRecordStatus.DRAFT.getCode()); } else { // 已归档病历禁止修改 if (MedicalRecordStatus.ARCHIVED.getCode().equals(record.getStatus())) { throw new BusinessException(病历已归档无法修改); } } // 2. 拷贝常规字段并设置状态 BeanUtils.copyProperties(dto, record, id); if (MedicalRecordStatus.SUBMIT.equals(dto.getStatus())) { this.validateRecordForSubmit(record); record.setStatus(MedicalRecordStatus.SUBMITTED.getCode()); record.setSubmitTime(LocalDateTime.now()); } else { record.setStatus(MedicalRecordStatus.SAVED.getCode()); } this.saveOrUpdate(record); // 3. 保存医嘱先删除再插入保证一致性 if (dto.getPrescriptions() ! null) { prescriptionMapper.delete(new LambdaQueryWrapperPrescription() .eq(Prescription::getVisitId, dto.getVisitId())); for (PrescriptionDTO p : dto.getPrescriptions()) { Prescription entity new Prescription(); BeanUtils.copyProperties(p, entity); entity.setVisitId(dto.getVisitId()); prescriptionMapper.insert(entity); } } return record.getId(); } private void validateRecordForSubmit(MedicalRecord record) { if (StringUtils.isBlank(record.getChiefComplaint()) || StringUtils.isBlank(record.getPresentIllness())) { throw new BusinessException(主诉和现病史为必填项请补充后再提交); } if (StringUtils.isBlank(record.getInitialDiagnosis())) { throw new BusinessException(初步诊断不能为空); } } }逻辑说明整个方法添加Transactional(rollbackFor Exception.class)意味着病历主表和医嘱表的写入要么全部成功、要么全部回滚不会出现「病历存了、医嘱没存」的不一致。步骤 2 的SUBMIT状态判断与第 3 章的状态机规则对应提交前调用validateRecordForSubmit做业务校验而不是让数据库去承受不完整的脏数据。医嘱采用「先删后插」的方式简化了更新逻辑——你不需要在更新时逐条对比哪些医嘱被删了、哪些改了因为一次就诊的医嘱数量通常不大整体替换的代价可接受。SecurityUtils.getCurrentUserId()是一个工具方法从 Spring Security 的SecurityContextHolder中取当前登录用户 ID后台自动记录经治医生前端不需要传递doctorId这能防止越权。如果你没有接入 Security可以临时从请求头里取用户标识但正式论文/答辩建议保留 Security 方案。4.3 Spring Security 配置登录、JWT 与角色权限电子病历系统必须控制访问权限核心是「医生能编辑自己的病历护士只能查看管理员能管理用户」。用 Spring Security JWT 实现无状态认证下面是配置类和过滤器链的核心片段Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf - csrf.disable()) .sessionManagement(session - session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/captcha).permitAll() .requestMatchers(/api/nurse/**).hasRole(NURSE) .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated()) .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }参数说明SessionCreationPolicy.STATELESS表示不创建 HttpSession每个请求都通过 JWT 识别身份requestMatchers按 URL 前缀配置角色权限/api/nurse/**只允许护士角色访问。在 JwtAuthenticationFilter 中从请求头的Authorization: Bearer token解析用户 ID 和角色放入SecurityContextHolder后续 Service 层即可拿到当前用户。实际调试时最容易出的问题是 CORS 配置。前端 Vue3 运行在http://localhost:5173后端是http://localhost:8080跨域请求会被浏览器拦截。需要加一个 CorsFilter 或在 Security 链上配置http.cors(cors - cors.configurationSource(corsConfigurationSource())); Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; }注意addAllowedOriginPattern(*)与setAllowCredentials(true)可以共存在 Spring 6.1 以上如果使用旧版 SpringaddAllowedOrigin(*)与allowCredentials(true)同时出现会报Cannot allow credentials for wildcard origin。这也是 SpringBoot3 升级后经常出现的兼容性问题提前在代码里规避可以省去很多调试时间。4.4 病历查询接口分页、条件组合与就诊历史回放病历查询包括两种高频场景一是列表页按患者姓名、科室、时间范围查病历二是详情页查某患者所有历史病历。第一个场景直接用 MyBatis-Plus 分页第二个场景注意要按visit_time倒序回放。以下是一个带条件的查询方法Override public PageResultMedicalRecordVO queryRecordPage(RecordQueryDTO dto) { LambdaQueryWrapperMedicalRecord wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(dto.getPatientName()), MedicalRecord::getPatientName, dto.getPatientName()) .eq(dto.getDepartmentId() ! null, MedicalRecord::getDepartmentId, dto.getDepartmentId()) .ge(dto.getStartTime() ! null, MedicalRecord::getCreateTime, dto.getStartTime()) .le(dto.getEndTime() ! null, MedicalRecord::getCreateTime, dto.getEndTime()) .orderByDesc(MedicalRecord::getCreateTime); PageMedicalRecord page this.page(new Page(dto.getPageNum(), dto.getPageSize()), wrapper); // 转换为 VO关联查询 patient 表和 doctor 表补充姓名 ListMedicalRecordVO voList page.getRecords().stream() .map(record - convertToVO(record)) .collect(Collectors.toList()); return new PageResult(voList, page.getTotal()); }逻辑说明wrapper.like(condition, column, value)的第一个参数是条件判断只有传入了对应参数才拼接该查询条件避免需要写多套 if/else。orderByDesc让最新的病历排在最前符合医生查看患者历史的习惯。MedicalRecordVO里除了主表字段还带patientName、doctorName、departmentName等冗余展示字段需要通过关联查询或批量查询补充。这里有一个常见的设计痛点medical_record表里没有直接存patient_name而列表页需要显示患者名。解决方案有两种一是建表时冗余一个patient_name字段写入时从 patient 表拷过来二是查询时 join patient 表。冗余适合「列表展示频繁、数据量不大」的场景但要注意患者改名后冗余字段会过期。毕业设计建议直接 joinSQL 不复杂也显得你懂范式。5. Vue3 前端实现从登录到病历编辑的完整交互链路5.1 创建 Vue3 项目并引入 Element Plus用 Vite 创建 Vue3 项目是最快的方式npm create vitelatest emr-frontend -- --template vue cd emr-frontend npm install npm install element-plus axios pinia vue-router参数说明--template vue生成基础 Vue3 项目element-plus提供组件库axios发 HTTP 请求pinia做全局状态管理存登录信息和用户角色vue-router做路由。不推荐用 Vue CLIwebpack创建新项目Vite 启动速度更快开发体验更接近 2025 年的主流实践。项目结构建议按模块组织src/ api/ # 按功能分模块的接口封装 medicalRecord.js auth.js views/ Login.vue dashboard/Dashboard.vue record/RecordList.vue record/RecordEdit.vue stores/ user.js # Pinia 用户状态 router/ index.js components/ PatientInfoCard.vue DiagnosisSelect.vue5.2 axios 拦截器与登录状态保持以下是src/api/request.js的核心代码import axios from axios import { ElMessage } from element-plus import router from ../router import { useUserStore } from ../stores/user const request axios.create({ baseURL: http://localhost:8080/api, timeout: 10000 }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response.status 401) { ElMessage.error(登录已过期请重新登录) router.push(/login) } else if (error.response.status 403) { ElMessage.error(没有权限执行此操作) } else { ElMessage.error(error.response?.data?.message || 服务器异常) } return Promise.reject(error) } ) export default request逻辑说明请求拦截器统一加 JWT避免每个接口手动设置请求头响应拦截器统一处理错误状态码前端业务代码里不需要到处 try/catch 网络错误。注意401和403的区别——前者是未认证跳转登录页后者是已认证但权限不足只给提示。这个区分在医疗系统中尤其重要医生和护士看到的结果不一样。5.3 病历编辑页组合式 API 实现草稿自动保存病历编辑页是最复杂的视图用两个组合式函数拆分逻辑useRecordData负责病历数据的加载和保存useAutoSave负责自动保存的防抖和定时器控制。// composables/useRecordData.js import { ref, onMounted } from vue import { saveMedicalRecord, fetchRecordDetail } from ../api/medicalRecord export function useRecordData(visitId) { const record ref({ id: null, visitId: visitId, chiefComplaint: , presentIllness: , pastHistory: , physicalExam: , initialDiagnosis: , prescriptions: [] }) const loading ref(false) async function loadRecord() { loading.value true try { const data await fetchRecordDetail(visitId) if (data data.id) { record.value { ...data } } } finally { loading.value false } } async function doSave(status) { const resp await saveMedicalRecord({ ...record.value, status: status }) record.value.id resp.id return resp } onMounted(loadRecord) return { record, loading, doSave } }// composables/useAutoSave.js import { watch, onUnmounted } from vue import { ElMessage } from element-plus export function useAutoSave(record, doSave, delay 30000) { let timer null watch(() JSON.stringify(record.value), () { clearTimeout(timer) timer setTimeout(async () { try { await doSave(1) // 1 暂存 } catch (e) { ElMessage.warning(自动保存失败请检查网络) } }, delay) }) onUnmounted(() clearTimeout(timer)) }逻辑说明useRecordData把数据加载和保存封装成一个整体返回给页面组件JSON.stringify作为 watch 的监听来源能捕获对象深层字段的变化但要注意它会在每次输入时触发所以自动保存必须加 30 秒防抖避免输入停顿不够就频繁调接口。delay参数默认 30000毫秒实际部署时可调整——如果医院网络条件差可以增加到 60 秒如果你们要求「输入完立即同步」就改成手动提交为主自动保存为辅。页面里这样组合使用script setup import { useRecordData } from ../composables/useRecordData import { useAutoSave } from ../composables/useAutoSave const { record, doSave } useRecordData(props.visitId) useAutoSave(record, doSave) /script所有字段通过v-model绑定到record对象的属性上Element Plus 的el-input和el-select会直接修改这个对象。无论你在主诉、现病史还是诊断里打字都会进入同一个自动保存流程这是组合式 API 典型的优势——逻辑被拆成独立的「可复用单元」而不是在组件内部堆一大段methods。5.4 路由守卫与医疗数据权限页面前端路由需要根据用户角色控制页面可见性。医生能看到「病历管理」「医嘱管理」护士只能看「患者列表」和「病历详情只读」管理员有「用户管理」和「数据统计」。// router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /record-list, component: () import(../views/record/RecordList.vue), meta: { roles: [DOCTOR] } }, { path: /nurse-view, component: () import(../views/NurseView.vue), meta: { roles: [NURSE] } } ] const router createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from, next) { const userStore useUserStore() if (to.path ! /login !userStore.token) { next(/login) } else { const roles to.meta.roles if (roles !roles.includes(userStore.role)) { next(/403) } else { next() } } })逻辑说明meta.roles在路由上声明允许访问的角色beforeEach里先检查是否登录再检查角色。前端守卫只影响页面展示真正的数据安全还是依赖第 4 章的 Spring Security 后端校验——前端守卫被绕过比如直接改路由或调接口时后端会返回 403。这一点建议在论文里写清楚前端权限是体验优化后端权限才是安全边界。6. 部署方案与问题排查本地跑通后怎么验证和展示6.1 用 IDEA 启动 SpringBoot3 与前端联调的配置在本地联调时我习惯把 SpringBoot 后端跑在8080端口Vite 前端跑在5173端口。前端request.js的baseURL指向http://localhost:8080/api跨域配置已在后端完成。如果启动后端时发现端口被占用在application.yml里改端口server: port: 8080 servlet: context-path: / spring: datasource: url: jdbc:mysql://localhost:3306/emr_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver参数说明serverTimezoneAsia/Shanghai必须加否则插入DATETIME时可能报时区错误useUnicode和characterEncoding保证中文不乱码。建议 MySQL 8.0 以上字符集选utf8mb4才能存储特殊符号和完整的简体中文。如果你使用 Maven 启动后端时遇到「Could not find artifact org.springframework.boot:spring-boot-starter-parent」之类的错误先检查网络仓库配置再确认 IDEA 的 Maven 是否设置了 JDK 17 作为运行环境。SpringBoot3 项目默认要求 JDK 17IDEA 里如果项目 SDK 还是 JDK 1.8启动会直接报UnsupportedClassVersionError。6.2 预置测试数据与演示脚本为了答辩时演示流畅建议在数据库里预置三组数据一个门诊患者有近期两次就诊记录、一个住院患者病历已提交、状态为 2、一个测试医生账号。以下是插入数据的示例 SQLINSERT INTO patient (id, name, id_card, phone, gender) VALUES (1, 张伟, 110101199001011234, 13800001111, 1), (2, 李静, 110101199505056789, 13800002222, 2); INSERT INTO visit (id, patient_id, visit_type, visit_time, department_id, status) VALUES (1, 1, 1, 2025-01-05 09:30:00, 3, 2), (2, 2, 2, 2025-01-06 14:00:00, 5, 1); INSERT INTO medical_record (id, visit_id, doctor_id, chief_complaint, present_illness, past_history, physical_exam, initial_diagnosis, status) VALUES (1, 1, 1, 头痛3天, 患者3天前无明显诱因出现头痛呈胀痛休息后缓解不明显, 否认高血压、糖尿病, 神清双瞳等大等圆颈软心肺腹未见明显异常, 偏头痛, 2), (2, 2, 1, 咳嗽伴发热2天, 2天前受凉后出现咳嗽咳黄痰伴发热最高38.5℃, 吸烟史10年, 双肺呼吸音粗可闻及湿啰音, 社区获得性肺炎, 1);演示时可以按这条链路走用医生账号登录 → 打开患者列表 → 选择张伟的问题 → 查看 1 月 5 日的门诊病历 → 尝试修改已提交的病历系统提示不可直接改→ 打开李静的住院病历 → 补充医嘱并暂存 → 提交。这个流程覆盖了系统的主要功能答辩时能引导评委看到状态流转和权限控制。6.3 常见报错场景与快速定位方法报错信息关键定位方向建议处理Failed to configure a DataSourceapplication.yml中数据库配置未生效检查连接串、用户名密码、MySQL 服务是否启动java.lang.ClassNotFoundException: javax.servlet.*引用了基于 javax 的旧依赖检查是否误用了旧版javax库SpringBoot3 必须用jakartaError creating bean with name jwtAuthenticationFilter过滤器 Bean 注入依赖缺失确认 JWT 工具类是否加了Component注解Access to XMLHttpRequest has been blocked by CORS policy后端未开启跨域参考 4.3 小节的CorsConfigurationSource配置Uncaught TypeError: Cannot read properties of undefined (reading id)前端接口返回数据结构和预期不一致打印 axios 响应数据确认是response.data还是data.data6.4 把项目打包成可演示的完整服务毕业设计最终提交可能需要一个「一运行就能看到效果」的交付物。前端可以构建成静态文件后由 SpringBoot 托管cd emr-frontend npm run build # 生成的 dist/ 目录拷贝到后端 src/main/resources/static/ 下这样重新打包后端 jar访问http://localhost:8080就会自动加载前端页面不需要再单独启动 Vite。打包前记得修改前端baseURL如果前端和后端同源baseURL直接设置为/api即可如果跨域才保留完整地址。SpringBoot 会把static目录下的index.html作为欢迎页Vue Router 使用createWebHistory时刷新深层路径可能 404需要在后端加一个 forward 到index.html的 Controller 或改用createWebHashHistory()后者虽然 URL 带#但更省事适合演示环境。6.5 电子病历系统的边界哪些功能不建议在毕业设计里做可能有人会问要不要做电子签名、要不要接 HIS 系统、要不要做 CDSS 临床决策支持这些在真实产品里很重要但在毕业设计中时间和精力有限建议做「小而完整」而不是「大而残缺」。把权限、状态流转、结构化病历、医嘱管理、历史回放做透已经能拿到不错的评价电子签名涉及 CA 证书和合规HIS 对接需要外部接口CDSS 需要知识库和规则引擎——这些可以作为「未来展望」写进论文结尾而不是硬塞进代码里。如果你学有余力可以在「数据统计」页做几个图表统计病历提交量、科室分布、诊断排名满足管理员的查看需求。前端用 ECharts后端提供一个聚合查询接口既提现全栈能力又不需要引入大数据组件。最后一个要注意的坑把写好的 SQL 脚本、初始化数据、前端构建产物都放进项目根目录下的docs/或sql/文件夹并写一个简短的 README 说明启动步骤。答辩时评委很可能让你现场演示如果你不在场时环境起不来有一个清晰的启动文档会省去很多尴尬。数据源密码、端口等配置用环境变量或配置项区分「开发环境」和「演示环境」不要在代码里硬编码。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门