ARTICLE DETAIL

资讯详情

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

Java+Vue体检医生端源码实战:架构、事务与部署

Java+Vue体检医生端源码实战:架构、事务与部署 简介一款基于Java及Vue框架的东软体检医生端设计源码面向体检服务管理系统开发者与Java/Vue全栈学习者提供完整的前后端分离实现方案。后端Java源文件承担体检流程的业务处理、数据操作与接口实现前端Vue组件体现组件化设计思路二者结合可高效支撑医生端日常业务场景。压缩包共368个文件、约10.46MB主体为129个Java源文件与8个Vue组件另有CSS/LESS/SCSS样式、HTML/JavaScript页面交互逻辑以及XML配置、SQL脚本和图片素材等资源。目前已有490人学习下载适合正在做体检系统课题或希望参考企业级全栈项目结构的读者。项目附带Maven构建配置、依赖锁定文件及说明文档目录结构清晰能够帮助快速启动、理解项目脉络无论是课程设计、毕业设计还是企业级项目原型都具有较高参考价值也便于在此基础上扩展新功能模块。1. 基于Java及Vue框架的东软体检医生端源码这套工程到底解决什么体检高峰期的内科诊室一上午五六十个体检者等着录入血压、心肺听诊结果界面卡一下、保存慢几秒队伍就开始躁动。这套基于Java及Vue框架的东软体检医生端设计源码正是把体检中心医生端的核心环节——登录鉴权、体检队列、检查项录入、科室小结、危急值上报——用 Spring Boot Vue MySQL Redis 完整落地的可运行工程。它不是一份讲概念的文档而是能直接启动、能改业务、能接真实数据库的源码包。适合三类人一是医院信息科或医疗软件公司的工程师在做体检系统的二次开发二是需要完整前后端分离案例做毕业设计或项目实战的学生三是想了解体检业务流程怎么转成代码结构的从业者。前端 Vue 页面、后端接口、数据库脚本都齐拿到的不是空中楼阁而是可以对着改的工程骨架。接下来我会按选型、后端、前端、踩坑、上线验证的顺序拆尽量把每处代码为什么这么写讲透。2. 技术选型与工程骨架Spring Boot Vue MySQL 的模块边界在哪拿到源码先别急着npm install先把工程结构和选型逻辑搞清楚后面改起来才有方向。这一章从技术栈为什么这么选、目录怎么分层、数据库怎么拆三块讲。2.1 为什么是 Spring Boot Vue而不是其他组合后端用 Spring Boot 是体检医生端这类后台管理系统最常见的方案没有之一。Spring Boot 的起步依赖把 Tomcat 内嵌、数据源配置、日志框架都打包好了一个java -jar就能起服务不需要额外装容器。体检中心的业务规模决定了它不需要微服务——一个科室级系统接口量撑死几十个强行拆成 Spring Cloud 各服务之间的网络开销和运维成本反而拖慢开发。MyBatis-Plus 在源码里承担数据访问层它和 Spring Boot 的配合很成熟分页插件、乐观锁、字段自动填充都是关键功能。前端选 Vue 的原因也很直接医生端是典型的表单密集型后台Element UI 组件库直接提供了表格、弹窗、级联选择这些现成组件。Vue 2 的响应式机制在若干年的生态积累下非常稳定各种踩坑记录都能在网上查到配合 Vue Router 做菜单路由、Vuex 存登录态开发效率比手写 DOM 高出几个量级。数据库用 MySQL Redis 的搭配MySQL 存体检业务数据Redis 存 token 和缓存热点数据这是体检系统里性价比最高的组合。整套源码是一个标准的单体应用别把它想复杂了。2.2 工程结构前后端分目录后端按 Controller-Service-Mapper 分层源码解压后是一个前后端分离的目录结构后端和前端各自独立成工程用接口沟通。我拆过的源码包里这套的目录层次很规范先给你一张结构图healthcheck-doctor/ ├── backend/ # Spring Boot 后端工程 │ ├── src/main/java/com/health/ │ │ ├── controller/ # 接口层登录、体检记录、科室小结 │ │ ├── service/ # 业务层事务、校验、异常处理 │ │ ├── mapper/ # MyBatis-Plus 数据访问层 │ │ ├── entity/ # 数据库实体类 │ │ ├── config/ # 拦截器、跨域、Jackson 配置 │ │ └── util/ # JWT、日期等工具类 │ └── src/main/resources/ │ ├── application.yml # 数据源、Redis、端口等配置 │ └── mapper/ # 自定义 SQL 的 XML 文件 ├── frontend/ # Vue 前端工程 │ ├── src/ │ │ ├── api/ # axios 接口封装 │ │ ├── views/ # 登录、体检录入、体检队列、报告页 │ │ ├── router/ # vue 路由配置 │ │ └── store/ # Vuex 用户登录态 │ └── vue.config.js # 开发代理、打包配置 └── sql/ # 建表脚本和初始字典数据这种分法的好处是职责边界清楚。controller只做参数接收和结果包装不写业务逻辑service层挂事务和核心判断mapper层只管 SQL。我一般拿到源码会先看controller里的接口列表快速知道这个系统有哪些功能再到service看每个接口的业务规则最后才去翻表结构。如果你看到某个controller里塞了一大段业务代码那这个工程后期维护基本是噩梦。2.3 数据库核心表设计主表、明细表、科室小结怎么拆体检业务的数据结构特点是一次体检对应多个检查项每项有自己的结果值、单位、参考范围和异常标记。如果把所有字段堆进一张表扩展一个新检查项目就得改表加字段所以源码里拆成了主表和明细表。核心建表脚本长这样CREATE TABLE physical_exam_record ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, pid VARCHAR(32) NOT NULL COMMENT 体检者编号, exam_date DATE NOT NULL COMMENT 体检日期, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待检 1已检 2总检中 3已完成, dept_code VARCHAR(32) DEFAULT NULL COMMENT 当前操作科室, version INT NOT NULL DEFAULT 1 COMMENT 乐观锁版本号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_pid_date (pid, exam_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT体检记录主表; CREATE TABLE physical_exam_item ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, record_id BIGINT NOT NULL COMMENT 关联主表ID, item_code VARCHAR(32) NOT NULL COMMENT 检查项目编码, item_name VARCHAR(64) NOT NULL COMMENT 项目名称, result_value VARCHAR(64) DEFAULT NULL COMMENT 检查结果值, unit VARCHAR(16) DEFAULT NULL COMMENT 单位, ref_range VARCHAR(64) DEFAULT NULL COMMENT 参考范围, abnormal_flag TINYINT DEFAULT 0 COMMENT 0正常 1异常 2危急, sort_no INT DEFAULT 0 COMMENT 排序号, PRIMARY KEY (id), KEY idx_record (record_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT体检检查明细表;主表一条记录代表一个人一次体检明细表按项目拆行。abnormal_flag是核心字段医生录完结果后由前端或后端根据参考范围自动判定总检医生再根据这些标记写总检建议。这里最要注意的是加索引的位置明细表按record_id建索引因为明细行只会在详情页被查列表页永远只查主表主表按pid exam_date建联合索引是因为要快速查出某个人某天是否已经建档。科室小结我建议单独拆表不要塞进主表因为小结包含文本内容且每个科室都要写自己的小结结构化数据归结构化文本归文本。3. 登录鉴权与体检记录事务JWT 拦截器和批量保存的落地写法后端是这套源码的核心登录鉴权决定了谁能用系统体检记录保存的事务决定了数据会不会丢。这一章把这两个核心环节的代码拆开讲。3.1 JWT 登录与 Redis 黑名单无状态鉴权的体检场景落地体检医生端不需要复杂的 OAuth2JWT Redis 是最简洁的方案。源码里 JWT 工具类生成 token 时会带上两个关键信息医生 ID 和科室编码这样后端在处理小结提交时可以直接从 token 里取科室防止前端伪造科室编号。public class JwtUtil { private static final String SECRET health-check-secret; private static final long EXPIRE_MINUTES 120; public static String createToken(String doctorId, String deptCode) { return Jwts.builder() .claim(doctorId, doctorId) .claim(deptCode, deptCode) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_MINUTES * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET.getBytes(StandardCharsets.UTF_8)) .compact(); } }这段代码里SECRET和EXPIRE_MINUTES在真实项目中一定要挪到配置文件里不能写死在类里。过期时间 120 分钟对体检场景偏紧上午班四小时医生录到一半 token 过期被踢下去很影响工作建议放到 480 分钟左右同时让前端在 token 失效前主动续期。JWT 有个固有问题——签发后无法主动让它失效体检系统又有「护士站把医生顶下线」这种强退需求所以源码里把 token 同时写一份到 Redis退出时标记黑名单public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(401, 未登录); } // 先查 Redis 黑名单再解析 JWT if (redisTemplate.hasKey(token:black: token)) { throw new BusinessException(401, 登录已失效请重新登录); } Claims claims JwtUtil.parseToken(token); request.setAttribute(doctorId, claims.get(doctorId)); request.setAttribute(deptCode, claims.get(deptCode)); return true; } }拦截器里把doctorId和deptCode放到 request 属性中后续 Controller 直接取不用再解析一次 token。这个模式在体检系统里非常实用比如「只有内科医生能提交内科小结」这类的权限校验就在 Service 层拿 request 里的deptCode和当前小结所属科室比对即可。3.2 体检记录批量保存主表 明细表的事务边界体检记录的保存是整套系统最容易出问题的接口。医生端一次提交会同时产生一条主表记录和几十条明细记录任何一条明细失败整体都要回滚否则会出现主表有数据、明细缺几行的情况这种脏数据让总检医生查半天也查不出结论。源码里的事务实现如下Transactional(rollbackFor Exception.class) public Long saveExamRecord(ExamRecordDTO dto) { LocalDate today LocalDate.now(); if (recordMapper.countByPidAndDate(dto.getPid(), today) 0) { throw new BusinessException(该体检者今天已有记录不能重复建档); } ExamRecord record new ExamRecord(); record.setPid(dto.getPid()); record.setExamDate(today); record.setStatus(0); record.setVersion(1); recordMapper.insert(record); for (ExamItemDTO item : dto.getItems()) { ExamItem entity new ExamItem(); entity.setRecordId(record.getId()); entity.setItemCode(item.getItemCode()); entity.setResultValue(item.getResultValue()); entity.setRefRange(item.getRefRange()); entity.setAbnormalFlag(markAbnormal(item)); itemMapper.insert(entity); } return record.getId(); }Transactional把整个方法框在一个事务里事务边界不是越大越好也不是越小越好。这里主表插入 明细循环插入必须同生共死所以放在一个事务方法里是合理的。但要注意如果dto.getItems()有几百条数据for 循环逐条 insert 性能堪忧常见做法是改成了 MyBatis-Plus 的批量插入或分批插入saveBatch分批大小建议 100 条一批。countByPidAndDate这个查重逻辑是个细节但必须做——体检者可能因为排队时间长先离开换个窗口又重新建档不查重的话一天能建出好几条重复记录。markAbnormal是判定异常的方法逻辑上应该把参考范围上下限和单位带入判断而不是前端传什么就存什么前端传值容易被篡改。3.3 科室小结与总检建议结构化存储的字段设计每个科室的医生在完成检查后要写自己的小结比如内科小结「血压偏高建议复查」外科小结「体格检查未见明显异常」。源码里没有为每个科室单独建接口而是统一用一张科室小结表通过dept_code区分数据结构如下{ recordId: 10234, deptCode: NEIKE, summaryText: 血压偏高建议复查, suggestText: 低盐饮食规律作息定期监测血压, templateFlag: true }summaryText存的是纯文本不是 HTML。体检报告最终要对接打印模板纯文本可以直接嵌入 PDF 或打印组件如果是 HTML 还得考虑样式注入和 XSS 攻击问题。templateFlag标记这条小结是否来自科室模板方便医生下次快速调用。这里有个实际坑多科室医生可能同时打开同一个体检者内科正在写小结外科也在写如果直接对同一行做UPDATE后提交的会把先提交的覆盖掉。所以科室小结表要么按人 科室做唯一约束要么在 Update 语句的 where 条件里带上科室编码确保只更新自己科室的那一行。我在源码里看到的是后一种方案UPDATE dept_summary SET summary_text ? WHERE record_id ? AND dept_code ?这个 where 条件是保命的。4. Vue 前端的路由守卫与接口联调从开发代理到打包部署前端部分直接影响医生的日常操作体验。这一章讲路由守卫怎么控登录态、体检录入页怎么做防重复提交、以及从开发环境到生产部署的完整通道。4.1 Vue 路由守卫与 Axios 拦截器token 失效时只跳一次登录体检系统的前端和后端是分开部署的前端所有接口依赖 token 认证。路由守卫保证未登录用户进不了业务页面Axios 响应拦截器统一处理 token 过期两段代码缺一不可。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) return } if (to.path /login token) { next(/) return } next() })这段 vue 路由守卫的逻辑不复杂没有 token 就去登录页有 token 访问登录页就跳到首页。真正要小心的是 Axios 拦截器里的「重复跳转」问题。医生端页面通常会同时发好几个接口请求token 过期时后端会对每个请求都返回 401如果不做处理弹窗会连续弹四五次路由会连续跳转好几次。源码里的写法值得借鉴service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { if (!window.__logoutFlag) { window.__logoutFlag true localStorage.removeItem(token) router.replace(/login) } setTimeout(() { window.__logoutFlag false }, 1000) } return Promise.reject(error) } )window.__logoutFlag是个简易全局锁第一个 401 触发跳转后1000 毫秒内的其他 401 不再重复触发。这个锁不能放在组件里因为多个组件各有一份实例变量锁不住也不能放在 Vuex 里因为 Vuex 状态刷新就没了挂在window上最稳。4.2 体检录入页动态表格防重复提交与大数据量卡顿录入页是医生停留时间最长的界面一个体检者几十个项目每个项目一行输入框。源码里用的是 Element UI 的el-table配合v-model绑定每行数据结构大致是这样的el-table :dataitemList border el-table-column label项目名称 propitemName width180/el-table-column el-table-column label结果值 template slot-scopescope el-input v-modelscope.row.resultValue placeholder请输入结果/el-input /template /el-table-column el-table-column label异常标记 width100 template slot-scopescope el-tag :typescope.row.abnormalFlag 1 ? danger : success {{ scope.row.abnormalFlag 1 ? 异常 : 正常 }} /el-tag /template /el-table-column /el-table保存按钮这里有一个非常容易翻车的细节——医生录入时习惯连续敲回车或双击保存按钮如果按钮没有防重复提交同一个体检记录会被提交两次后端即使做了当天查重第二次提交返回异常也不会把第一次的结果覆盖但网络慢的时候会导致页面出现两条一样的记录。源码里的保存方法是典型的 loading 锁async handleSave() { if (this.saving) return this.saving true try { await saveExamRecord(this.form) this.$message.success(保存成功) } finally { this.saving false } }this.saving这个布尔值在请求期间置 true按钮变成 loading 状态同时阻止再次点击。这里要注意finally里重置saving否则接口报错后按钮会永久锁死。表格大数据量卡顿是另一个高频场景体检项目超过 200 行时全量渲染会有明显延迟我一般建议前端做分页展示每页 50 条或者用 el-table 的虚拟滚动组件别在模板里一次性渲染几百行输入框浏览器扛不住。4.3 开发联调与生产部署Nginx 代理和 vue 打包放进 springboot开发阶段最烦的是跨域。前端跑在 8080 端口后端跑在 8081 端口直接请求会触发 CORS。源码里用的方案是 vue.config.js 的 devServer 代理module.exports { devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } }这样前端请求/api/login实际转发到http://localhost:8081/loginpathRewrite把/api前缀剥掉后端接口不用做任何改动。生产部署有两条路。第一条是 Nginx 反向代理前端npm run build后的 dist 目录放到 Nginx同时把/api代理到后端 jar 的端口server { listen 80; location / { root /data/healthcheck/html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081; } }第二条路是 vue 打包放进 springboot 中把 dist 目录整个复制到backend/src/main/resources/static重新打包后 jar 自带页面和接口一个进程搞定。这种方式适合医院内网服务器资源紧张、不想单独维护 Nginx 的场景。但要注意vue 打包放进 springboot 时如果前端路由用了 history 模式刷新页面会 404因为 Spring Boot 默认的静态资源映射找不到/record/list这种路径。要么把路由改成 hash 模式要么在后端加一个转发 Controller 把非接口路径都转到 index.html。我一般建议小项目直接 hash 模式省事且稳定。5. 体检医生端常见问题排查五条真实踩坑记录源码能跑通是一回事部署到真实体检中心不出问题是另一回事。这一章把我拆过同类系统时踩过的、以及源码里容易埋雷的点整理成五条记录每条按现象、原因、解决的思路讲透。5.1 时间字段差 8 小时年龄算错一岁现象体检记录保存成功后列表页显示的创建时间比北京时间早 8 小时体检者出生日期在前端算出来年龄总是少一岁。原因后端LocalDateTime序列化默认不带时区返回给前端的是类似2025-06-01T08:00:00的字符串前端直接new Date()解析时浏览器会按本地时区再转一次加上数据库连接串里的serverTimezone没配存进去的值本身就偏了。解决在application.yml里固定 Jackson 的时区和日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 datasource: url: jdbc:mysql://localhost:3306/healthcheck?serverTimezoneAsia/Shanghai前端拿到格式化后的字符串不要再用new Date()转直接当字符串显示。年龄也别在前端算让后端按exam_date减去birth_date返回数据库函数算日期最不容易出偏差。5.2 MyBatis-Plus 乐观锁失效后保存的覆盖先保存的现象内科医生和外科医生同时打开同一个体检者的记录内科先提交外科后提交内科写的小结在界面上消失了。原因实体类的version字段没有加Version注解或者updateById时前端没把 version 值传回来MyBatis-Plus 的乐观锁拦截器根本没生效。体检场景里两个科室同时操作一条记录是常态没有乐观锁必然互相覆盖。解决实体加Version配置乐观锁拦截器Version private Integer version; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }前端在保存时把记录行的version字段原样带上后端 update 时SET version version 1 WHERE id ? AND version ?更新行数为 0 就抛异常提示「数据已被他人修改请刷新后再试」。这个提示一定要写清楚不然医生不知道发生了什么。5.3 History 模式刷新白屏F5 按出了 404现象前端部署后点菜单正常但一按 F5 刷新体检队列页面就白屏或 404。原因vue 路由用了 history 模式路径是真实的 URL如/record/list刷新时浏览器向服务器请求这个路径Nginx 或 Spring Boot 的静态资源映射找不到对应的物理文件就返回了 404。解决Nginx 加try_files $uri $uri/ /index.html把所有匹配不到文件的请求都转回 index.html交给 vue 路由接管。如果是 vue 打包放进 springboot 的场景只能在VueRouter里把mode改成hash或者在后端加一个转发 Controller。hash 模式丑一点但胜在省心体检系统这种内网应用不追求 URL 美观稳是第一位的。5.4 列表页接口慢到 3 秒明细数据全量返回现象体检者列表页打开要 3 秒以上越翻越慢后台 MySQL 的 CPU 一直很高。原因列表接口的 SQL 把主表和明细表 JOIN 了一条主记录带几十行明细一起返回列表页其实只需要主表的体检者姓名、日期和状态明细根本用不上。再加上主表大字段没做索引覆盖ORDER BY create_time DESC走了全表排序。解决列表接口只查主表分页用 MyBatis-Plus 的分页插件明细改成点击「查看详情」时单独请求。给create_time加索引pid和exam_date的联合索引在查重时不走错路。记住一个原则列表接口只返回列表需要展示的列明细永远按需加载数据量大时这条能救回一半的性能问题。5.5 危急值上报重复推送检验科收到三条一样的消息现象一个危急值比如血糖 28.9 mmol/L被推送了三次检验科和总检医生都收到了重复提醒。原因医生在录入界面点了两次保存或者接口超时后前端自动重试后端上报危急值的接口没有做幂等校验——同一个记录同一天报了三次消息队列就发三条。解决在危急值上报表加唯一索引从数据库层面兜底ALTER TABLE critical_value_report ADD UNIQUE KEY uk_record_item_time (record_id, item_code, report_time);同时后端在插入前先查一次record_id item_code report_time组合是否已存在存在就直接返回成功告诉前端「已经报过了」。前端保存按钮做了 loading 锁后这个问题的触发频率已经降低但数据库唯一索引是最后的防线必须有。6. 进阶验证与上线检查让这套医生端在 Linux 服务器上稳定跑起来源码在本地能跑只是第一步真正考验人的是部署到 Linux 服务器后的稳定性。这一章讲两个我用得上的实战技巧多环境配置和上线前的自测清单。6.1 多环境配置与 Linux 启动脚本本地开发和医院生产环境的数据库地址、Redis 地址肯定不一样源码里如果只有一个application.yml每次部署都要改配置再重新打包太容易被改错。我在多套项目里的习惯做法是拆成application-dev.yml和application-prod.yml启动时用参数指定mvn clean package -DskipTests java -Xms512m -Xmx512m -jar healthcheck-backend.jar --spring.profiles.activeprodLinux 上还要挂后台运行配合日志输出nohup java -Xms512m -Xmx512m -jar healthcheck-backend.jar \ --spring.profiles.activeprod \ /data/healthcheck/logs/app.log 21 内存给我 512m 到 1g 之间。体检医生端并发量不大512m 起步够用给太多反而浪费服务器资源。nohup和保证 SSH 断开后进程不挂日志重定向到固定路径方便排错。6.2 接口联调自测清单与上线检查把这套源码推到生产前我建议按下面这张表强制走一遍自测这一套流程能筛掉大部分低级问题检查项操作预期结果时区配置保存一条体检记录看创建时间与北京时间一致不差 8 小时乐观锁两个浏览器打开同一记录先后保存后保存的提示数据已被修改路由刷新部署后按 F5 刷新列表页页面正常显示不白屏不 404危急值幂等同一记录连续上报两次第二次被拦截或提示已上报列表性能造 1 万条主表数据翻页单页响应小于 500ms我从这套源码里学到最深的一件事体检系统的问题大部分不在功能缺失而在数据一致性和部署细节。时区错、乐观锁失效、路由 404这三个问题任何一个都能让上线第一天就翻车。从那以后我每次接新的体检医生端源码都会强制走一遍这三步先改时区配置、再验证乐观锁、最后跑一次路由刷新测试。这三次确认没问题系统基本能安稳上线。希望帮到你。本文还有配套的精品资源点击获取
返回列表