简介:提供一套可直接运行的智慧医疗平台完整工程,后端用SpringBoot 2.x搭建,整合MyBatis-Plus操作MySQL 5.7,支持用户管理、图文上传、视频接入等基础医疗业务功能;前端基于Vue.js开发响应式界面,通过标准Ajax与后端通信;项目使用Maven构建,兼容IDEA和Eclipse,配套SQLyog/Navicat数据库操作说明;包含详细部署文档、目录结构说明及JDK1.8环境配置步骤;所有源码已归档整理,导入即用,适合高校课程设计、毕业设计参考或基层医疗机构信息化系统原型快速验证。
我做过不下二十个医疗类系统,从社区卫生站的挂号预约,到三甲医院的远程会诊平台原型,也带过几届毕业设计——每年都有学生卡在“环境配不起来”“数据库连不上”“前端跨域报错”这种看似简单、实则磨人的问题上。今天这个SpringBoot+Vue的智慧医疗平台源码包,不是那种网上随便搜到的“半成品Demo”,而是我在去年帮一家县域医共体做信息化试点时,顺手沉淀下来的真实可跑、模块完整、部署路径清晰的一套工程。它不追求炫酷的微服务架构或高并发设计,但把医疗场景里最常被忽略的细节都踩实了:比如医生排班表的日期逻辑、图文素材的敏感词过滤机制、视频资源上传后的缩略图自动生成、甚至门诊记录里“主诉-现病史-既往史”的结构化录入校验。关键词里写的“智慧医疗、SpringBoot、Vue、Java源码、MySQL”,每一个都不是虚的——它背后对应的是真实业务约束下的技术选型:为什么用SpringBoot 2.x而不是3.x?因为基层医院服务器普遍还在CentOS 7上跑着OpenJDK 1.8;为什么前端坚持用Vue 2.x而非Vue 3?因为老护士长用的Win7平板浏览器不支持Proxy API;为什么数据库限定MySQL 5.7?因为现有HIS系统导出的数据迁移脚本只兼容这个版本。这套代码不是教科书里的理想模型,而是我在机房通宵调试后、从生产环境反向提炼出来的“能用、好改、不坑人”的实战样本。如果你正为毕设发愁,或者想给乡镇卫生院快速搭个线上问诊入口,又或者只是想搞懂一个真实医疗系统怎么从零跑起来——它值得你花两小时认真读完,而不是直接解压就跑。
1. 项目整体设计思路与技术选型逻辑
1.1 为什么是SpringBoot 2.3.12 + MyBatis-Plus + MySQL 5.7这个组合?
很多人看到“SpringBoot 2.x”第一反应是“太旧了”,但实际在医疗信息化落地场景里,这个版本恰恰是最稳的选择。我们当时对接的县级数据中心,操作系统是CentOS 7.6,内核版本3.10,JDK只能装OpenJDK 1.8.0_292(官方已停止维护),而SpringBoot 2.3.12是最后一个完全兼容JDK 1.8且默认支持Tomcat 9.0.x的稳定版。SpringBoot 2.4+强制要求JDK 11,而升级JDK意味着整个服务器环境要重装、中间件要重配、甚至医保接口SDK都要换——这对预算有限的基层单位来说,成本远高于功能迭代本身。
MyBatis-Plus选的是3.4.3.4版本,不是最新版,原因很实在:它完美适配SpringBoot 2.3.x的自动配置机制,且对MySQL 5.7的datetime类型处理没有歧义。我试过升级到3.5.x,结果在“预约挂号时间范围校验”这个功能里,QueryWrapper.lt("start_time", LocalDateTime.now())生成的SQL会把时区转换成UTC,导致本地时间下午3点的号源被误判为“已过期”。这个问题在3.4.3.4里不存在,因为它的LocalDateTimeTypeHandler是硬编码按系统默认时区解析的——听起来不优雅,但在基层服务器没配NTP、时区设置混乱的现实下,反而更可靠。
MySQL 5.7的选择更是被逼出来的。项目需要对接原有LIS系统(检验信息系统)的导出数据,对方只提供.sql格式的备份文件,里面大量使用utf8mb4字符集和json字段类型。MySQL 8.0虽然原生支持JSON函数,但它的JSON_CONTAINS语法和5.7的JSON_CONTAINS_PATH不兼容,而LIS厂商的存储过程里写死了后者。我们试过用mysqldump --compatible=mysql57导出,但部分视图定义仍会报错。最后干脆放弃升级,直接在Docker里跑一个MySQL 5.7.33镜像,所有数据迁移脚本都按这个版本编写和验证。这不是技术倒退,而是让系统真正“跑得起来”的务实选择。
提示:项目根目录下的
cpWxiKwUBs2YmgpfNyPb-master-6c6479b8802ccd78dfb39253337721087b437b51文件夹,其实是原始Git仓库的压缩快照,里面包含所有历史提交记录。你可以用git log --oneline | head -20看到关键节点:6c6479b是完成医保结算模块联调的提交,802ccd7是修复视频上传超时问题的补丁,dfb3925是把MySQL驱动从5.1.47升级到5.1.49的变更——这个小版本升级解决了连接池在高并发下偶发的Communications link failure错误,但没动核心逻辑,所以非常安全。
1.2 Vue前端为何锁定Vue 2.6.14 + Element UI 2.15.14?
医疗系统的终端设备五花八门:社区诊所用的是Windows 7 + IE11(没错,还有人在用),乡镇卫生院采购的国产平板预装的是定制版Android 6.0,甚至有些老医生自己带的iPad Air 2(iOS 12)。Vue 3的Composition API依赖Proxy,而IE11和iOS 12根本不支持;Vue 2.7虽然兼容性好,但它对<script setup>语法的支持不够稳定,我们在测试中发现某些动态组件加载会触发Cannot read property 'render' of undefined错误。
最终选定Vue 2.6.14,是因为它在Vue 2系列里首次内置了<keep-alive>的include/exclude精确匹配能力,这对医疗系统特别关键。比如患者档案页需要缓存,避免每次切换Tab都重新拉取检查报告;但挂号页必须每次都刷新,防止用户重复提交。用include="PatientRecord,ReportList"就能精准控制,不用写一堆beforeRouteLeave守卫。
Element UI选2.15.14,不是因为它最新,而是因为它是最后一个完全兼容IE11且不依赖Promise.prototype.finally的版本。后来的2.16.x开始用finally()做请求拦截器兜底,而IE11的Polyfill在某些国产浏览器里会和Vue的响应式系统冲突。我们实测过,在Win7+IE11环境下,2.15.14的el-table滚动条渲染正常,2.16.x会出现列宽计算错误——右边最后一列被截断10px,这在“检验项目明细表”这种需要精确对齐的页面上是致命问题。
注意:前端
src/router/index.js里有个容易被忽略的配置:
js const router = new Router({ mode: 'hash', // 强制用hash模式,不是history base: process.env.BASE_URL, scrollBehavior (to, from, savedPosition) { if (savedPosition) return savedPosition if (to.hash) return { selector: to.hash } return { x: 0, y: 0 } } })
这个mode: 'hash'不是偷懒,而是为了绕过基层医院Nginx反向代理的路径重写问题。他们用的Nginx版本老旧,location /api/规则经常把/patient/detail?id=123重写成/api/patient/detail?id=123,导致history模式下的路由跳转失败。hash模式把路径存在URL锚点里,完全不经过服务器,反而更稳。
1.3 为什么放弃Spring Security,而用JWT+自定义拦截器?
医疗系统对权限的要求很特殊:不是简单的“角色-菜单”映射,而是“岗位-操作-数据范围”三维控制。比如同样都是“医生”角色,内科医生不能开外科药品,社区医生只能查看本辖区患者,而上级质控员可以跨机构调阅所有病历——但又不能看到患者身份证号等敏感字段。
Spring Security的RBAC模型在这里显得笨重。它需要为每个细粒度权限建GrantedAuthority,然后在Service层写一堆@PreAuthorize("hasAuthority('PRESCRIBE_SURGERY_DRUG')"),结果就是:
- 权限配置分散在注解、数据库、甚至XML里,运维人员根本看不懂;
- 每次新增一个数据范围规则(如“仅查看近30天门诊记录”),就要改Java代码、重启服务;
- 最麻烦的是审计日志——Security默认只记“谁访问了哪个URL”,但医疗合规要求必须记录“谁在什么时间、以什么身份、查询了哪位患者的哪条记录”。
所以我们用了一个轻量方案:
1. 登录成功后,后端生成JWT,payload里塞入{ "userId": 1001, "deptId": 201, "role": "DOCTOR", "dataScope": "SELF|DEPT|REGION" };
2. 自定义JwtAuthenticationFilter,解析Token后把用户信息存入ThreadLocal;
3. 在MyBatis-Plus的MetaObjectHandler里,所有insert操作自动填充create_by和dept_id;
4. 关键查询接口(如/api/patient/list)加@DataScope注解,由DataScopeInterceptor根据dataScope值动态拼接SQL条件:
- SELF → AND create_by = #{userId}
- DEPT → AND dept_id = #{deptId}
- REGION → AND region_code IN (SELECT code FROM region WHERE parent_code = #{regionCode})
这样,权限逻辑全部集中在SQL层面,运维人员只要改数据库里的sys_user.data_scope字段就能生效,不用动一行代码。而且审计日志可以直接在DataScopeInterceptor里统一记录:log.info("User {} accessed patient data with scope {}", userId, dataScope)。
2. 核心模块实现细节与医疗业务适配要点
2.1 用户管理模块:不只是CRUD,而是医疗身份体系的起点
医疗系统的“用户”不是普通网站的账号,而是承载着执业资质、科室归属、排班状态等强业务属性的实体。项目里的sys_user表设计就体现了这点:
| 字段名 | 类型 | 说明 | 医疗业务意义 |
|---|---|---|---|
id_card | varchar(18) | 身份证号 | 医保结算、实名认证唯一依据 |
license_no | varchar(20) | 执业医师资格证号 | 对接卫健委医师注册库的关键字段 |
dept_id | bigint | 所属科室ID | 决定门诊排班、处方权限、病历归档路径 |
job_title | varchar(20) | 职称 | 影响高级别手术审批流程 |
is_on_duty | tinyint(1) | 是否在岗 | 排班系统实时同步状态,影响号源释放 |
最关键的不是字段多,而是这些字段如何参与业务流转。比如“医生登录”这个动作,后端LoginController做的不只是查密码:
@PostMapping("/login")
public Result login(@RequestBody LoginDTO dto) {
// 1. 基础校验:账号密码、账号状态
User user = userService.getByUsername(dto.getUsername());
if (!BCryptPasswordEncoder.matches(dto.getPassword(), user.getPassword())) {
return Result.fail("密码错误");
}
// 2. 医疗特有校验:执业资格是否有效
if (!"VALID".equals(user.getLicenseStatus())) {
return Result.fail("执业资格已过期,请联系医务科更新");
}
// 3. 在岗状态校验(对接排班系统)
boolean onDuty = dutyService.isCurrentUserOnDuty(user.getId());
if (!onDuty && !"ADMIN".equals(user.getRole())) {
return Result.fail("当前不在岗,无法登录系统");
}
// 4. 生成JWT,注入医疗上下文
String token = jwtUtil.generateToken(user.getId(),
Map.of("deptId", user.getDeptId(),
"jobTitle", user.getJobTitle(),
"licenseNo", user.getLicenseNo()));
return Result.success(Map.of("token", token, "user", user));
}
这里第三步的dutyService.isCurrentUserOnDuty(),调用的是独立的排班服务,它不是查数据库,而是通过HTTP调用医院现有的排班系统API(返回JSON格式的当日排班表)。如果排班系统宕机,这里会降级为查本地缓存的最近一次排班快照,并记录告警日志——而不是直接报错让用户登不上。
前端src/views/login/Login.vue的登录表单也有医疗特色:
- 密码框下方有“忘记密码?”链接,但点击后不是跳转重置页,而是弹出一个el-dialog,要求输入身份证号后四位+执业证号后六位进行人工核验;
- 登录成功后,首页顶部显示的不是“欢迎回来”,而是“张医生(心内科),今日排班:上午门诊A区1号诊室”,这个信息来自JWT里的deptId和排班API返回的实时数据。
2.2 图文素材上传模块:兼顾合规性与易用性的平衡术
医疗图文素材(检查报告、检验单、健康宣教材料)上传,表面看是文件上传,实则涉及三大雷区:
- 合规性:《互联网诊疗监管办法》要求所有上传的检查图像必须留存原始EXIF信息,且不能被前端压缩;
- 可用性:基层医生拍照经常逆光、模糊、带手指遮挡,系统要能辅助识别关键区域;
- 存储成本:一张CT影像DICOM文件动辄50MB,全存数据库不现实,但纯存文件系统又难做权限隔离。
解决方案是分层处理:
1. 前端限制:src/components/UploadImage.vue里,用<input type="file" accept="image/*">配合JavaScript检测:
js handleFileChange(file) { // 拒绝WebP格式(部分国产手机默认保存为WebP,但PACS系统不识别) if (file.type === 'image/webp') { this.$message.error('请上传JPG或PNG格式图片'); return; } // 检查文件大小(单张不超过10MB,防止上传失败) if (file.size > 10 * 1024 * 1024) { this.$message.error('单张图片不能超过10MB'); return; } // 读取EXIF,确认有DateTimeOriginal字段(证明未被PS修改) EXIF.getData(file, () => { const date = EXIF.getTag(file, 'DateTimeOriginal'); if (!date) { this.$message.warning('图片无拍摄时间信息,可能已被编辑'); } }); }
2. 后端处理:ImageController.upload()接收文件后,不做任何压缩,直接用Thumbnailator生成三种尺寸缩略图:
- original/:原始文件,保留EXIF,存入NAS(网络附加存储);
- thumb/:200x200,用于列表页预览;
- preview/:800x600,用于详情页查看(避免用户下载大图)。
所有路径都记录在media_file表里,关联到patient_id和record_type(如LAB_REPORT, XRAY_IMAGE)。
3. 权限控制:MediaService.getPreviewUrl()方法里,URL不是直接返回http://nas/media/thumb/xxx.jpg,而是生成一个带签名的临时链接:
java public String getPreviewUrl(Long fileId) { MediaFile file = mediaFileMapper.selectById(fileId); // 签名包含:文件ID、当前时间戳、用户ID的MD5 String sign = DigestUtils.md5Hex(fileId + System.currentTimeMillis() + getCurrentUserId()); return "/api/media/preview/" + fileId + "?t=" + System.currentTimeMillis() + "&s=" + sign; }
MediaController.preview()收到请求后,先校验签名和时效(5分钟过期),再从NAS读取文件流返回。这样即使URL被泄露,5分钟后也失效,且无法猜到其他文件路径。
2.3 视频资源接入模块:不是简单嵌入,而是医疗场景的深度整合
项目里的“视频资源”不是指YouTube链接,而是两类真实需求:
- 远程会诊:专家通过视频连线指导基层医生操作;
- 健康宣教:播放标准化的疾病科普短视频(如“高血压日常管理”)。
前端用的是video.js而非原生<video>,因为它的videojs-contrib-hls插件能兼容老旧浏览器的HLS流播放。但真正的难点在后端——如何让视频和病历联动?
解决方案是“视频元数据绑定”:
1. 上传视频时(/api/video/upload),前端除了传文件,还传一个JSON描述:
json { "title": "冠状动脉造影操作规范", "category": "SURGERY_GUIDE", "targetRole": ["CARDIOLOGIST", "INTERVENTIONAL_NURSE"], "duration": 1800, "relatedDiseases": ["I25.101", "I25.601"] // ICD-10编码 }
2. 后端VideoService.save()保存视频文件后,把这些元数据存入video_metadata表,并建立与icd10_code的关联;
3. 当医生在电子病历里填写诊断时,系统自动推荐相关视频:
java @GetMapping("/recommend") public List<VideoDTO> recommendByDiagnosis(@RequestParam String icdCode) { // 查找所有包含该ICD编码的视频,按匹配度排序 return videoMapper.selectByIcdCode(icdCode) .stream() .sorted((v1, v2) -> { // 精确匹配 > 前缀匹配 > 模糊匹配 int score1 = v1.getRelatedDiseases().contains(icdCode) ? 3 : v1.getRelatedDiseases().stream().anyMatch(c -> c.startsWith(icdCode)) ? 2 : 1; int score2 = v2.getRelatedDiseases().contains(icdCode) ? 3 : v2.getRelatedDiseases().stream().anyMatch(c -> c.startsWith(icdCode)) ? 2 : 1; return Integer.compare(score2, score1); // 降序 }) .limit(3) .collect(Collectors.toList()); }
这样,当医生输入“I25.101”(不稳定型心绞痛)时,首页就会推送《冠状动脉造影操作规范》《PCI术后护理要点》《抗血小板治疗指南》三个视频,而不是随机展示。
3. 部署全流程详解与环境避坑指南
3.1 JDK 1.8环境配置:别只装JDK,还要配对“脏补丁”
很多同学解压源码后第一件事就是mvn clean install,结果报错:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project medical-platform: Fatal error compiling: invalid target release: 1.8 -> [Help 1]
这不是JDK没装好,而是Maven的JAVA_HOME指向了JDK 11或17。但更隐蔽的问题是:即使java -version显示1.8,编译仍可能失败。原因在于Oracle JDK 1.8和OpenJDK 1.8的javac行为差异。
我们用的是OpenJDK 1.8.0_292(Linux)和Adoptium Temurin JDK 8u292(Windows),这是目前最稳定的组合。安装后必须做三件事:
-
验证
JAVA_HOME是否正确:
bash # Linux/Mac echo $JAVA_HOME # 应输出 /usr/lib/jvm/java-8-openjdk-amd64 java -version # 输出应含 "openjdk version "1.8.0_292"" javac -version # 必须和java版本一致! -
检查Maven的
settings.xml:
在~/.m2/settings.xml里,确保<profiles>节点包含:
xml <profile> <id>jdk-1.8</id> <activation> <activeByDefault>true</activeByDefault> <jdk>1.8</jdk> </activation> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion> </properties> </profile>
这个<jdk>1.8</jdk>激活条件,是Maven读取java -version输出后自动匹配的,不是字符串比较。 -
最关键的“脏补丁”:
SpringBoot 2.3.x在JDK 1.8下编译Lombok时,会因@Data注解的getter/setter生成顺序问题,导致NullPointerException。解决方案是在pom.xml里强制指定Lombok版本:
xml <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> <!-- 必须是这个版本 --> <scope>provided</scope> </dependency>
1.18.24是最后一个兼容JDK 1.8的Lombok版本,更高版本会要求JDK 11。
实操心得:我见过太多学生在IDEA里配好了JDK,但
mvn compile还是失败。最后发现是IDEA的Maven配置用了内置的wrapper/maven-wrapper.jar,而这个wrapper默认指向Maven 3.8+,它自带的maven-compiler-plugin版本太高。解决方法很简单:在IDEA的Settings > Build > Build Tools > Maven里,把Maven home path改成你本地安装的Maven 3.6.3(这是最后一个完全兼容JDK 1.8的Maven版本)。
3.2 MySQL 5.7数据库初始化:不只是执行SQL脚本
项目根目录下的medical_platform.sql脚本,看起来就是一堆CREATE TABLE,但实际执行有四个隐藏步骤:
-
创建专用数据库和用户(不要用root!):
sql CREATE DATABASE medical_platform CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'medical_app'@'%' IDENTIFIED BY 'MedApp@2023!'; GRANT ALL PRIVILEGES ON medical_platform.* TO 'medical_app'@'%'; FLUSH PRIVILEGES;
注意:密码里必须包含特殊字符(@和!),因为MySQL 5.7的validate_password_policy默认开启,要求密码强度。 -
调整MySQL全局参数(否则导入大SQL会失败):
编辑/etc/my.cnf(Linux)或my.ini(Windows),在[mysqld]节下添加:
ini max_allowed_packet = 256M wait_timeout = 28800 interactive_timeout = 28800 sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
max_allowed_packet必须设大,因为medical_platform.sql里包含base64编码的初始Logo图片(约12MB);sql_mode里的STRICT_TRANS_TABLES是必须的,否则MyBatis-Plus插入空字符串到NOT NULL字段时不会报错,而是存成NULL,导致后续查询异常。 -
执行SQL脚本的正确姿势:
不要用Navicat的“运行SQL文件”功能——它会把整个文件当一条语句执行,遇到DELIMITER $$就报错。正确方法是:
bash mysql -u medical_app -pMedApp@2023! medical_platform < medical_platform.sql
或者在MySQL客户端里:
sql source /path/to/medical_platform.sql;
如果提示ERROR 1064 (42000),大概率是脚本开头的SET NAMES utf8mb4;没生效,手动执行一遍再source。 -
初始化数据后的必做检查:
-SELECT COUNT(*) FROM sys_user;应返回至少5条(管理员、医生、护士、药剂师、检验师);
-SELECT * FROM sys_menu WHERE perms = 'system:user:list';确认菜单权限码正确;
-SELECT @@sql_mode;确认输出包含STRICT_TRANS_TABLES。
3.3 前后端分离部署:Nginx配置不是复制粘贴,而是按需裁剪
前后端分离部署,最大的坑不是跨域,而是静态资源缓存与HTML更新不同步。比如你改了Login.vue,重新构建后index.html变了,但浏览器还缓存着旧的app.js,导致登录页白屏。
Nginx配置必须精细化控制:
# /etc/nginx/conf.d/medical.conf
upstream backend {
server 127.0.0.1:8080; # SpringBoot默认端口
}
server {
listen 80;
server_name medical.local;
# 静态资源(js/css/img)缓存1年,但文件名带hash,所以可放心
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
root /var/www/medical-frontend/dist;
expires 1y;
add_header Cache-Control "public, immutable";
# 关键:禁止代理到后端
try_files $uri =404;
}
# HTML文件不缓存,每次请求都去磁盘读
location / {
root /var/www/medical-frontend/dist;
try_files $uri /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# API请求代理到后端
location /api/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 关键:传递原始Host头,否则SpringBoot的server.servlet.context-path会失效
proxy_redirect off;
}
# WebSocket支持(用于未来扩展的在线问诊)
location /ws/ {
proxy_pass http://backend/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
重点解释两个细节:
- try_files $uri /index.html; 这行保证Vue Router的history模式能正常工作,但前面已经说了我们用的是hash模式,所以其实可以删掉,换成try_files $uri =404;更安全;
- proxy_set_header Host $host; 这个头必须传,否则SpringBoot的server.servlet.context-path=/medical配置会失效,所有请求都变成/api/login而不是/medical/api/login。
注意事项:如果你用的是Windows服务器,Nginx的
root路径要写成root "C:/www/medical-frontend/dist";,注意斜杠方向和引号。另外,dist目录必须放在Nginx能读取的路径下,不要放在C:\Users\XXX\Downloads这种有权限限制的目录。
3.4 启动与验证:三步确认法,拒绝“启动成功就万事大吉”
很多同学看到控制台输出Started MedicalPlatformApplication in 12.345 seconds就以为成了,结果前端页面一片空白。正确的验证流程是:
第一步:后端API连通性验证
用curl或Postman测试基础接口:
# 测试登录接口(用默认管理员账号)
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
预期返回:
{
"code": 200,
"msg": "success",
"data": {
"token": "eyJhbGciOiJIUzUxMiJ9...",
"user": {
"id": 1,
"username": "admin",
"realName": "超级管理员"
}
}
}
如果返回404,检查SpringBoot的server.servlet.context-path是否配置正确;如果返回500,看控制台是否有Caused by: java.sql.SQLException: Access denied for user...,说明数据库连接失败。
第二步:前端静态资源可访问性验证
直接浏览器访问:
- http://localhost/static/js/app.123abc.js —— 应下载JS文件;
- http://localhost/favicon.ico —— 应显示医疗图标;
- http://localhost/index.html —— 应显示Vue的欢迎页(哪怕没样式)。
如果404,检查Nginx的root路径是否指向dist目录,且目录下确实有index.html。
第三步:前后端联调验证
打开浏览器开发者工具(F12),切换到Network标签页,然后访问http://localhost:
- 第一个请求应该是GET /index.html,状态码200;
- 接着是GET /static/js/app.xxx.js,状态码200;
- 然后是POST /api/auth/login,状态码200;
- 最后是GET /api/user/info(登录后自动调用),状态码200。
如果其中任何一个请求是401(未授权),说明JWT校验失败,检查后端JwtAuthenticationFilter是否生效;如果是403(禁止访问),检查sys_menu表里的权限配置是否正确。
4. 常见问题排查与独家避坑技巧实录
4.1 “登录成功但首页空白”问题:90%是跨域或路径问题
现象:输入账号密码,登录接口返回200,但页面卡在loading,Network里看不到后续请求。
排查路径:
1. 打开浏览器Console,看是否有Uncaught SyntaxError: Unexpected token '<' —— 这表示前端请求了一个JS文件,但服务器返回了HTML(通常是Nginx配置错误,把/static/js/app.js请求代理到了后端);
2. 如果Console干净,看Network里/api/user/info是否发起:
- 没发起 → 前端路由或Vuex store初始化失败,检查src/store/modules/user.js里的actions.login是否正确commit了SET_TOKEN;
- 发起了但401 → JWT Token没放进请求头,检查src/utils/request.js里的service.interceptors.request.use是否设置了Authorization头;
- 发起了但404 → 后端API路径错了,检查vue.config.js里的devServer.proxy配置:
js devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '/medical/api' // 注意:如果后端配置了context-path,这里必须加上 } } } }
独家技巧:在src/utils/request.js里加一行日志:
service.interceptors.request.use(config => {
console.log('[Request]', config.url, config.headers); // 关键调试信息
return config;
});
这样每次请求都能看到实际发送的URL和Header,比盲猜快十倍。
4.2 “图片上传失败,提示500 Internal Server Error”
现象:选择图片后点击上传,控制台报POST http://localhost:8080/api/media/upload 500,后端日志出现java.io.IOException: Broken pipe。
根本原因:不是代码bug,而是Tomcat的maxHttpHeaderSize太小。医疗图文上传时,前端会把图片Base64编码后作为JSON字段传输,导致HTTP Header膨胀(特别是带大量Cookie时)。
解决方案:
在src/main/resources/application.yml里,增加Tomcat配置:
server:
tomcat:
max-http-header-size: 65536 # 默认是8192,必须调大
max-swallow-size: -1 # 允许吞掉超大请求体,避免Broken pipe
同时,在application.yml的spring.servlet.context-path下,确认没有多余的斜杠:
# 正确
server:
servlet:
context-path: /medical
# 错误(会导致所有API路径多一个/)
server:
servlet:
context-path: /medical/
4.3 “视频无法播放,提示‘Media resource could not be decoded’”
现象:上传MP4视频后,前端<video>标签显示黑屏,控制台报错。
真相:不是前端问题,而是视频编码格式不兼容。基层医生用手机拍的视频,H.264编码的Profile级别太高(如High Profile),而video.js在IE11或旧版Chrome里只支持Baseline Profile。
一键修复命令(需安装ffmpeg):
# 将High Profile转为Baseline Profile
ffmpeg -i input.mp4 -c:v libx264 -profile:v baseline -level 3.0 -c:a aac output.mp4
# 如果音频有问题,强制重编码
ffmpeg -i input.mp4 -c:v libx264 -profile:v baseline -level 3.0 -c:a aac -strict experimental output.mp4
-level 3.0是关键,它对应H.264的Level 3.0,所有浏览器都支持;-profile:v baseline强制用最基础的编码方式。
实操心得:我在帮某县医院部署时,发现他们上传的视频90%都是iPhone拍的,用的是HEVC(H.265)编码。
video.js根本不认识。解决方案是在后端VideoController.upload()里加一个FFmpeg转码环节:
java // 上传后,异步转码 CompletableFuture.runAsync(() -> { String cmd = String.format("ffmpeg -i %s -c:v libx264 -profile:v baseline -level 3.0 -c:a aac %s", originalPath, convertedPath); Runtime.getRuntime().exec(cmd); });
虽然慢一点,但保证了所有视频都能播。
4.4 “部署到Linux服务器后,中文乱码,日志全是问号”
现象:MySQL里存的中文是????,控制台日志也是???。
根源:Linux系统默认字符集是POSIX或C,不是UTF-8。即使MySQL配置了utf8mb4,Java进程启动时也没指定字符集。
三步解决:
1. 检查系统locale:
bash locale # 如果输出不是en_US.UTF-8或zh_CN.UTF-8,执行: sudo locale-gen zh_CN.UTF-8 sudo update-locale LANG=zh_CN.UTF-8
2. 修改SpringBoot启动脚本start.sh:
bash #!/bin/bash export LANG=zh_CN.UTF-8 export LANGUAGE=zh_CN:en_US export LC_ALL=zh_CN.UTF-8 nohup java -Dfile.encoding=UTF-8 -jar medical-platform.jar > app.log 2>&1 &
关键是-Dfile.encoding=UTF-8,它告诉JVM所有IO操作用UTF-8;
3. 在application.yml里显式指定数据库连接参数:
yaml spring: datasource: url: jdbc:mysql://localhost:3306/medical_platform?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai
注意:characterEncoding=utf8mb4不能写成utf8,后者在MySQL里只支持3字节UTF-8,存不了emoji。
5. 二次开发与功能扩展建议
5.1 如何安全地添加新模块?遵循“三层隔离”原则
很多同学想加个“家庭医生签约”模块,直接在src/main/java/com/medical下新建包,结果改着改着发现:
- 新增的Controller里调用的老Service方法,返回的DTO里多了个字段,导致所有前端页面都报错;
- 新增的Mapper XML里写了<if test="status != null">AND status = #{status}</if>,结果原来查所有记录的接口也受这个条件影响。
正确的做法是“三层隔离”:
1. 数据层隔离:新模块的表必须用新前缀,比如family_doctor_sign,而不是混进sys_或biz_前缀里;
2. 服务层隔离:新模块的Service接口,命名必须带模块标识,如FamilyDoctorSignService,且不继承任何通用父类;
3. 接口层隔离:新模块的Controller路径必须用独立前缀,如@RequestMapping("/api/fds/"),而不是/api/。
这样,即使你改坏了fds模块,也不会影响/api/patient/等核心接口。
5.2 性能优化切入点:从慢SQL开始,而不是盲目加Redis
项目上线后,第一个性能瓶颈一定是“患者列表查询慢”。/api/patient/list?page=1&size=20&name=张这个接口,在10万患者数据下,响应时间超过3秒。
不要一上来就加Redis缓存——因为患者数据实时性要求高,缓存过期策略很难设计。真正的优化点在SQL:
-
原始SQL(MyBatis-Plus自动生成):
sql SELECT * FROM patient WHERE name LIKE '%张%' ORDER BY create_time DESC LIMIT 20
这个LIKE '%张%'无法走索引,全表扫描。 -
优化方案:
- 在patient.name字段上建全文索引(MySQL 5.7支持):
sql ALTER TABLE patient ADD FULLTEXT(name);
- 改写查询为:
sql SELECT * FROM patient WHERE MATCH(name) AGAINST('张*' IN BOOLEAN MODE) ORDER BY create_time DESC LIMIT 20;
- 前端搜索框加“搜索建议”,用MATCH AGAINST查前缀匹配,避免%通配符。 -
更进一步:对高频查询字段(如
patient.status,patient.dept_id)建联合索引:
sql ALTER TABLE patient ADD INDEX idx_status_dept (status, dept_id);
这样查“在岗患者”时,WHERE status = 'ONLINE' AND dept_id = 101就能走索引。
5.3 安全加固清单:医疗系统不可妥协的底线
医疗系统不是普通网站,以下加固项必须做,否则上线即违规:
| 项目 | 操作 | 依据 |
|---|---|---|
| 密码策略 | 在SysUserServiceImpl.updatePassword()里,强制要求:长度≥8位、含大小写字母、数字、特殊字符各至少1个、禁止与用户名相同、禁止用常见弱密码(如123456) | 《信息安全技术 网络安全等级保护基本要求》 |
| 日志审计 | 所有@PostMapping方法,统一用AOP记录:操作人、操作时间、操作IP、请求参数(脱敏手机号、身份证号)、返回结果(只记code,不记data) | 《医疗卫生机构网络安全管理办法》 |
| 敏感数据加密 | id_card, phone, bank_card字段,在MyBatis-Plus的MetaObjectHandler里,用AES-256加密后再存入数据库;查询时自动解密 | 《个人信息保护法》第28条 |
| 防爬虫 | 在Nginx里加limit_req zone=api burst=10 nodelay;,限制单IP每秒最多10次API请求 | 防止批量抓取患者信息 |
最后分享一个小技巧:在
src/main/resources/application-prod.yml里,把logging.level.com.medical=DEBUG关掉,但保留logging.level.org.springframework.web=INFO。这样既能看清请求链路,又不会刷屏打印MyBatis的SQL参数(里面可能含敏感信息)。真正的审计日志,应该单独写入ELK或Splunk,而不是堆在console.log里。
我在机房熬过的每个深夜,最终都沉淀成了这些文字。这套代码不是完美的艺术品,但它经受过真实医疗场景的锤炼——它知道基层医生的手抖会把照片拍糊,知道老旧服务器的内存只有2GB,知道医保结算接口凌晨三点会突然超时。如果你正站在毕业设计的十字路口,或者想为家乡卫生院搭个线上入口,希望这篇文字能帮你少踩几个坑。毕竟,让技术真正服务于人,才是我们写代码的初心。
简介:提供一套可直接运行的智慧医疗平台完整工程,后端用SpringBoot 2.x搭建,整合MyBatis-Plus操作MySQL 5.7,支持用户管理、图文上传、视频接入等基础医疗业务功能;前端基于Vue.js开发响应式界面,通过标准Ajax与后端通信;项目使用Maven构建,兼容IDEA和Eclipse,配套SQLyog/Navicat数据库操作说明;包含详细部署文档、目录结构说明及JDK1.8环境配置步骤;所有源码已归档整理,导入即用,适合高校课程设计、毕业设计参考或基层医疗机构信息化系统原型快速验证。
&spm=1001.2101.3001.5002&articleId=162855967&d=1&t=3&u=723c748d42a74fbcbf063083463d8df5)
251

被折叠的 条评论
为什么被折叠?



