恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

SpringBoot2+Vue3+MyBatis-Plus构建心脏病数据分析系统实战

  • 首页
  • 资讯中心
  • /
  • SpringBoot2+Vue3+MyBatis-Plus构建心脏病数据分析系统实战

相关资讯

Redis六层学习路径:从基础命令到源码剖析的进阶指南 2026/10/5 10:55:57
CCNA备考避坑指南:PDF题库与实操断层深度解析 2026/10/5 10:55:57
计算机网络考试题库怎么用?三遍刷题闭环与避坑指南 2026/10/5 10:55:57

最新资讯

互补格雷码与相移码结合的结构光相位解包裹实现
深度学习心电异常检测:从信号预处理到部署的完整实践指南
从自动生成到源码解析:OpenZeppelin 5.x ERC20合约实战
基于SpringBoot与微信小程序的大学生餐厅点餐系统实战
Spring循环依赖源码解析:三级缓存能解决什么,解决不了什么
插件机制深度解析与加载失败排查:从 IAR 到前端工具链的实战指南

今日推荐

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单
YOLOv5 OBB旋转框训练实战:从DOTA数据准备到调参避坑全流程
Zeron 终端、Worktree 与 Diff 面板:像 IDE 一样查看并驱动你的代码变更

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

SpringBoot2+Vue3+MyBatis-Plus构建心脏病数据分析系统实战

发布时间:2026/10/5 10:55:57
SpringBoot2+Vue3+MyBatis-Plus构建心脏病数据分析系统实战 1. 项目背景与整体技术选型为什么是这套组合1.1 一个医学数据项目最怕的到底是什么先坦白一件事我今年用 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 完整做了一套心脏病数据分析系统刚开始以为最难的是算法真正动手以后才明白这个项目最难的其实是三件事——数据字段的标准化、统计口径的统一、以及前后端联调时的类型匹配问题。医学数据不像电商订单它的字段命名混乱、缺失值多、指标单位不统一如果前期数据库设计不够稳后面所有分析逻辑都会被带偏。这套系统最终解决了什么问题简单来说把心脏病患者的病历特征数据导入系统通过 Web 页面完成数据的增删改查、多条件筛选、按年龄段/性别/风险等级聚合统计并用图表展示出来。适合谁参考一是正在做毕业设计或课程项目的同学二是想快速搭一套带分析能力的后台管理系统的 Java 开发者三是对 SpringBoot2 和 Vue3 前后端分离架构还不够熟练、想找一个完整项目练手的人。这套系统用的技术组合非常“主流但不过时”技术组件版本选型说明后端框架SpringBoot 2.7.x稳定、生态成熟、资料多持久层MyBatis-Plus 3.5.x内置通用 CRUD免去大量重复 SQL前端框架Vue3 ViteComposition API 写业务逻辑更紧凑UI 组件库Element Plus表格、表单、图表布局都有现成方案数据库MySQL 8.0窗口函数、CTE、JSON 支持都比 5.7 好用可视化ECharts 5医疗数据常用的折线图、饼图、柱状图都能覆盖1.2 这套组合的取舍逻辑很多人问我SpringBoot3 都出来了为什么还用 SpringBoot2我的理由很直接——生态兼容性。SpringBoot2.7 是 2.x 的收官版本大量第三方 starter 都基于它适配网上能搜到的问题解决方案也最多。而 SpringBoot3 强制要求 JDK17很多学校机房和老项目还是 JDK8一旦升上去部署环境的选择面就窄了。Vue3 这边没什么好犹豫的Vue2 已经停止维护新项目直接用 Vue3 是必然选择。不过 Vue3 和 Vue2 的差异对新手来说确实是个坎比如ref和reactive的区别、setup语法糖里的生命周期、组件通信方式的调整。这些在后面的前端章节我会详细展开。MySQL8.0 的选择更有意思——它默认的认证插件是caching_sha2_password不是老版本的mysql_native_password这就导致很多老版本驱动连接时会报Unable to load authentication plugin。项目里如果遇到这个问题要么升级驱动到 8.x要么在创建用户时指定老插件。这个坑我在后面第 5 章专门写了一段排查链路。选 MyBatis-Plus 则纯粹是为了效率。传统 MyBatis 写单表 CRUD 需要手写一堆 XML 和映射MyBatis-Plus 直接提供BaseMapper内置selectById、selectList、insert、updateById等方法还能用LambdaQueryWrapper做条件构造大幅度减少样板代码。但它也有边界——复杂的多表关联、动态排序、超大分页这些场景需要谨慎使用不能把它当成万能工具。2. 后端核心搭建SpringBoot2 MyBatis-Plus 的快速实现2.1 数据库设计心脏病数据表该如何建模这套系统的核心表设计我之前反复调整过三次最后稳定为一张主表加若干维度字段。主表heart_disease_record的字段结构大致如下CREATE TABLE heart_disease_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, patient_name VARCHAR(64) NOT NULL COMMENT 患者姓名, age INT NOT NULL COMMENT 年龄, gender TINYINT NOT NULL COMMENT 性别1男 2女, chest_pain_type TINYINT COMMENT 胸痛类型1典型心绞痛 2非典型 3非心绞痛 4无症状, resting_blood_pressure INT COMMENT 静息血压(mmHg), serum_cholesterol INT COMMENT 血清胆固醇(mg/dl), fasting_blood_sugar TINYINT COMMENT 空腹血糖120mg/dl1是 0否, resting_ecg TINYINT COMMENT 静息心电图0正常 1ST-T异常 2左心室肥厚, max_heart_rate INT COMMENT 最大心率, exercise_angina TINYINT COMMENT 运动诱发心绞痛1是 0否, st_depression DECIMAL(4,2) COMMENT 运动ST段压低值, slope TINYINT COMMENT ST段斜率1上升 2平坦 3下降, num_vessels TINYINT COMMENT 彩色多普勒检查血管数, thal TINYINT COMMENT 地贫类型3正常 6固定缺陷 7可逆缺陷, risk_level TINYINT COMMENT 风险等级0低危 1中危 2高危, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) COMMENT 心脏病患者数据记录表;这个建模有几个关键决策点。第一risk_level既支持手动标定也可以根据后面统计逻辑动态计算这样系统既能当录入工具又能当分析工具。第二所有分类字段都用TINYINT存编码值而不是直接存中文这样前端下拉框可以统一走字典映射避免数据库里出现各种乱七八糟的中文别名。第三create_time和update_time必须保留——数据分析里非常看重时间维度比如按星期、按月看新增病例趋势。性别、胸痛类型、风险等级这些字段我写了一个DictService统一管理字典映射前端需要的字典项都从后端接口拉取。这么做比前端写死要累一点但后期改文案、加属性都不动前端代码只改数据库和接口即可。2.2 通用CRUD服务基于MyBatis-Plus的工具类实现MyBatis-Plus 的通用 CRUD 服务网上习惯说“通用 crud 服务:基于 mybatis-plus 工具类实现无状态增删改查”实际上就是一个极简的 Service 封装。我的做法是创建一个BaseServiceT包住BaseMapperT的常用方法public class BaseServiceT { Autowired protected BaseMapperT baseMapper; public T getById(Long id) { return baseMapper.selectById(id); } public boolean save(T entity) { return baseMapper.insert(entity) 0; } public boolean update(T entity) { return baseMapper.updateById(entity) 0; } public boolean delete(Long id) { return baseMapper.deleteById(id) 0; } public PageT pageQuery(PageT page, LambdaQueryWrapperT wrapper) { return baseMapper.selectPage(page, wrapper); } }心跳记录的业务服务继承这个BaseService再扩展自己的方法Service public class HeartRecordService extends BaseServiceHeartDiseaseRecord { public IPageHeartDiseaseRecord queryWithCondition(HeartQueryDTO dto) { LambdaQueryWrapperHeartDiseaseRecord wrapper new LambdaQueryWrapper(); wrapper.eq(dto.getGender() ! null, HeartDiseaseRecord::getGender, dto.getGender()) .eq(dto.getRiskLevel() ! null, HeartDiseaseRecord::getRiskLevel, dto.getRiskLevel()) .between(dto.getMinAge() ! null dto.getMaxAge() ! null, HeartDiseaseRecord::getAge, dto.getMinAge(), dto.getMaxAge()) .ge(dto.getMinHeartRate() ! null, HeartDiseaseRecord::getMaxHeartRate, dto.getMinHeartRate()) .orderByDesc(HeartDiseaseRecord::getCreateTime); return pageQuery(new Page(dto.getCurrent(), dto.getSize()), wrapper); } }这里最关键的技巧是LambdaQueryWrapper 的条件构造。eq(condition, column, value)的第一个参数是布尔值为 true 才拼接 SQL 条件。这样前端传筛选条件时不需要写一串 if-else 来拼接 SQL代码看起来干净也不容易漏条件。还有一个细节版本翻过车Page类的getTotal()在默认情况下是查总数的但如果用了selectPage时底层 SQL 很短MyBatis-Plus 会自动优化 count 查询语句去掉 order by。不过如果你自己在 wrapper 里加了groupBy或者自定义了 count 逻辑分页总数可能会不对。遇到这种场景老老实实用自定义 SQL 或者单独写 count 查询。2.3 复合查询与统计分析接口的落地CRUD 只是地基这套系统真正有价值的接口是统计类接口。比如按年龄段分布GetMapping(/statistics/age-distribution) public ResultListAgeStatVO ageDistribution() { ListAgeStatVO list baseMapper.selectMaps( new QueryWrapperHeartDiseaseRecord() .select(CASE WHEN age 30 THEN 20-30 WHEN age BETWEEN 30 AND 40 THEN 30-40 WHEN age BETWEEN 40 AND 50 THEN 40-50 WHEN age BETWEEN 50 AND 60 THEN 50-60 ELSE 60 END AS age_group, COUNT(*) AS count) .groupBy(age_group) .orderByAsc(age_group) ).stream().map(row - new AgeStatVO( (String) row.get(age_group), ((Long) row.get(count)).intValue() )).collect(Collectors.toList()); return Result.success(list); }注意这里用selectMaps而不是selectList因为聚合查询返回的不是实体类结构Map 更适合动态字段。selectMaps 查出来的 count 字段默认是Long类型转 Integer 要小心空指针。性别与风险等级的交叉分析我用了 MySQL8.0 的窗口函数比原来的多次 group by 简洁不少SELECT gender, SUM(CASE WHEN risk_level 0 THEN 1 ELSE 0 END) AS low_level, SUM(CASE WHEN risk_level 1 THEN 1 ELSE 0 END) AS mid_level, SUM(CASE WHEN risk_level 2 THEN 1 ELSE 0 END) AS high_level FROM heart_disease_record GROUP BY gender;这类 SQL 写在 mapper XML 里然后用MapKey映射结果。前端拿到数据直接灌给 ECharts 的堆叠柱状图。做统计接口时我有一个习惯所有统计接口都以 VO 对象返回而不是直接返回 Map。虽然 Map 快但你有多少字段前端完全不可预期后端重构字段名时前端照样挂。VO 能定死接口契约也方便 Swagger 自动生成文档。3. 数据分析模块不只是增删改查3.1 心脏病特征指标的统计逻辑数据分析模块是整个系统最能体现价值的部分也是区别于普通后台管理系统的地方。医生或研究人员导入一批病历后最想看的不是哪条数据对不上而是这批数据整体长什么样。我的系统实现了三类核心分析第一类是单项指标分布分析。比如“静息血压分布”我先做了分桶逻辑低于 100、100-120、120-140、140-160、160 以上每个桶统计一条记录。这个逻辑用 SQL 的CASE WHEN做分桶聚合比前端拿到全部数据再分组要高效得多尤其当数据量到几万条时前端浏览器根本卡不动。第二类是风险层级联动分析。把risk_level作为主维度分别统计不同风险层的平均年龄、平均最大心率、平均 ST 段压低值。这三个数字能很直观地反映高层级和低层级患者的心率特征差异。我之前测试时发现高危组的平均最大心率比低危组低了约 15 次/分钟这个结果在医学上是有解释的——心率变异性降低往往意味着自主神经调节功能受损。第三类是异常值筛查逻辑。在列表页做一个“可疑异常”开关后端按经验阈值自动标记符合条件的记录比如静息血压高于 180 或低于 60、血清胆固醇大于 350、最大心率大于 200 的记录。这些阈值不能算医疗诊断标准但可以作为数据质量预检提醒录入人员去核实原始数据是否录错。正常值参考范围我在系统里做了一个config_table存储阈值管理员可以在页面上修改阀值配置而不需要改代码。3.2 图表接口如何按年龄段、性别、风险等级聚合图表接口和表格列表接口不同它需要一次返回多维度数据前端拿过去直接就能渲染。我把这类接口定位为“聚合仪表盘数据”接口核心设计思想是一次请求多维返回。以首页仪表盘为例我定义了一个DashboardVOpublic class DashboardVO { private Long totalRecords; private Long highRiskCount; private Double highRiskRate; private ListNameCountVO ageGroupDistribution; private ListNameCountVO genderDistribution; private ListRiskGenderVO riskGenderMatrix; private ListNameCountVO chestPainDistribution; private ListMonthCountVO monthlyTrend; }每个字段对应一个聚合查询。这样前端页面加载时只调用一个/api/dashboard/overview接口一次性拿到全部图表数据。好处是联调简单前端不用发 6 个请求拼数据坏处是如果某个查询特别慢会拖慢整个页面。目前实测在 10 万条记录内这套聚合接口响应时间控制在 800ms 左右。这里必须说一个 MyBatis-Plus 的注意事项多个聚合查询尽量不要写在同一个 XML 里复用 resultTypeMap因为不同查询返回的列名不一致很容易混。我选择为每个聚合查询建立独立的 Mapper 方法虽然代码稍多但每个查询都能单独调试、单独优化。还要注意日期聚合按月份统计时MySQL8.0 可以直接用DATE_FORMAT(create_time, %Y-%m)来分组。但中文字段别名和前端 key 映射要提前对齐我前端monthlyTrend里每一项就是{ month: 2025-01, count: 88 }。4. 前端工程Vue3 组合式 API 搭建后台管理界面4.1 从 ref/reactive 到 Axios 封装Vue3 和 Vue2 最大的分水岭就是组合式 API。Vue2 里写业务逻辑data、methods、computed、watch 各占一块业务相关代码被拆散Vue3 用setup语法糖后同一个业务域的状态和函数可以物理集中。比如心电图表单script setup import { ref, reactive, onMounted } from vue; const formRef ref(null); const loading ref(false); const dictData ref({}); const form reactive({ patientName: , age: null, gender: 1, chestPainType: null, restingBloodPressure: null, maxHeartRate: null, riskLevel: null }); async function loadDicts() { const { data } await dictApi.getAll(); dictData.value data; } async function saveRecord() { await formRef.value.validate(); loading.value true; try { await heartApi.save(form); ElMessage.success(保存成功); emit(refresh); } finally { loading.value false; } } onMounted(loadDicts); /script关于ref和reactive我的使用习惯是这样的复杂对象用reactive单个基础类型或者需要整体替换引用的对象用ref。网上那句“ref 万能对象”其实说的就是ref内部也是用reactive实现的但你直接访问reactive对象的属性不需要.value而ref必须要.value这个区别新手经常会栽跟头。Axios 封装我采用类似下面的结构重点在于统一处理错误提示和 tokenimport axios from axios; import { ElMessage } from element-plus; const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 15000 }); service.interceptors.response.use( (response) { const res response.data; if (res.code ! 200) { ElMessage.error(res.message || 请求失败); return Promise.reject(new Error(res.message)); } return res; }, (error) { ElMessage.error(error.message || 网络异常); return Promise.reject(error); } ); export default service;后端接口统一返回ResultT结构code200表示成功。这个约定很重要前后端一旦养成习惯联调效率能提升不少不用每次都对响应格式扯皮。4.2 数据可视化页面的实现要点ECharts 是数据分析系统前端的重头戏。我在首页放了五个图风险等级环形图、性别堆叠柱状图、年龄段分组柱状图、月度趋势折线图、胸痛类型饼图。实现上最核心的是封装一个ChartPanel组件template div refchartRef classchart-panel/div /template script setup import * as echarts from echarts; import { ref, onMounted, onBeforeUnmount, watch } from vue; const props defineProps({ option: { type: Object, required: true } }); const chartRef ref(null); let chartInstance null; function render() { if (!chartInstance) { chartInstance echarts.init(chartRef.value); } chartInstance.setOption(props.option, true); } function resize() { chartInstance chartInstance.resize(); } onMounted(() { render(); window.addEventListener(resize, resize); }); onBeforeUnmount(() { window.removeEventListener(resize, resize); chartInstance chartInstance.dispose(); }); watch(() props.option, render, { deep: true }); /script这里有几个细节新手容易漏setOption的第二个参数传true表示完全替换配置而不是合并如果图表类型变化后不传这个参数旧的配置会残留window.resize事件监听必须在卸载前移除不然切路由后图表实例占用的内存不会被释放还有容器必须是块级元素且设置了高度否则 ECharts 默认偷偷给你个 0 高度图表完全不显示。我在做环形图和柱状图联动时还踩了一个坑——图例点击默认是隐藏对应 series但这个交互行为和筛选联动逻辑会冲突。最终我直接关闭了默认图例行为改成了点击图例触发自定义筛选。4.3 动态表单与筛选条件联动心脏病数据录入表单里有很多联动逻辑选了“胸痛类型典型心绞痛”后某些字段会高亮提示风险等级选了“高危”表单背景会变为浅红色。这些其实都是动态表单的常见需求。我在录入页面用了 Element Plus 的el-form动态校验通过rules对象和v-if条件渲染实现。更灵活的场景是“动态添加删除 form 表单一行数据”比如录入并发症信息时每条记录可能有 0-5 个不等的并发症描述不应该在数据库主表里预留 5 个字段而是建一个子表heart_complication前端用数组渲染多行输入template div v-for(item, index) in complicationList :keyindex el-input v-modelitem.name placeholder并发症名称 / el-input v-modelitem.detail placeholder详情描述 / el-button typedanger clickremoveComplication(index)删除/el-button /div el-button typeprimary clickaddComplication添加并发症/el-button /template这个场景配合 Vue3 的reactive数组来管理非常顺手。提交时以嵌套 JSON 格式传给后端后端用TableField(exist false)标记这个字段不是主表字段然后单独批量插入子表。这就是典型的“一主多子”表单结构。筛选条件联动也类似用户先选“风险等级”再选“年龄段”前端把筛选条件对象实时传给列表接口后端用上一章提到的 LambdaQueryWrapper 条件构造器动态拼 SQL。前端要注意的是避免每敲一个字都发请求我做了 500ms 的防抖处理。5. MySQL8.0 安装配置与 Docker 部署实践5.1 MySQL8.0 安装与连接踩坑MySQL8.0 的安装本身不算复杂但很多朋友在本地开发时会踩几个明确的坑。这里我按顺序整理一套实际验证过的步骤。第一步下载安装。Windows 用户推荐去官网下载 MySQL Installer 8.0.x 社区版选择 Server only 即可。macOS 用户如果没有特殊要求用 Homebrew 最省事brew install mysql8.0。第二步初始化密码问题。安装过程中会让你设置 root 密码但如果忘了密码后续连接会报Access denied for user rootlocalhost。解决办法是用mysqld --initialize-insecure重新初始化一个空密码的数据目录再ALTER USER修改。不要试图去翻旧笔记找什么重置密码的偏方这个做法在 MySQL8.0 是最稳妥的。第三步驱动匹配。项目连接 MySQL8.0 之前确认mysql-connector-java版本。5.x 的驱动连接 8.0 数据库大概率报Communications link failure或认证协议错误。SpringBoot2.7 项目里推荐使用dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency注意 8.0 的 artifactId 是mysql-connector-j而不是老版本的mysql-connector-java在 Maven 中央仓库已经迁移了。同时 datasource 配置里建议显式指定serverTimezoneAsia/Shanghai否则会因为时区问题报The server time zone value CST is unrecognizedspring: datasource: url: jdbc:mysql://localhost:3306/heart_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.DriverallowPublicKeyRetrievaltrue这一个参数也要注意如果不开连接时经常报Public Key Retrieval is not allowed。原因跟 8.0 的缓存 sha2 认证方式有关开发环境可以放心开启生产环境要谨慎评估。5.2 Docker 方式部署 MySQL8.0 并跑通项目如果你不想在本地机器直接装 MySQLDocker 是最快的一条路。我用一条命令拉起 MySQL8.0 容器docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEheart_db \ -v mysql_data:/var/lib/mysql \ -v /etc/localtime:/etc/localtime:ro \ mysql:8.0这里有几个生产环境必须注意的细节。第一必须挂载数据卷否则容器一旦删掉数据库文件直接消失。-v mysql_data:/var/lib/mysql会创建一个命名卷容器删除后数据仍在。第二/etc/localtime挂载是为了容器内时间与宿主机保持一致。数据库记录时间如果偏差 8 小时分析图表会有严重误导。第三指定版本 tag写成mysql:8.0而不是mysql:latest避免某天latest变成 9.x 导致行为不一致。拉起来之后进入容器确认一下配置docker exec -it mysql8 mysql -uroot -p123456然后执行CREATE USER heart% IDENTIFIED BY heart123; GRANT ALL PRIVILEGES ON heart_db.* TO heart%; FLUSH PRIVILEGES;用独立业务账号连接数据库是好习惯不要所有环境都拿 root 连。项目真正上线时%这种允许任意 IP 的账号也会被替换成指定网段。Docker 方式还有一个优势——多个项目测试环境互不干扰。我之前同时跑过 MySQL5.7 和 8.0 两个容器端口映射为 3306 和 3307各自跑不同版本的项目完全不需要卸载重装。5.3 后端部署到服务器时的配置清单如果要把这套系统部署到 Linux 服务器我的操作顺序是安装 JDK8 或 JDK11配置 JAVA_HOME。SpringBoot2.7 用 JDK8 完全没问题生产环境我也推荐 JDK8稳定、调优资料多。安装 Maven用于打包mvn clean package -DskipTests产出 jar 包。打包前记得把application-prod.yml的数据库地址换成服务器 IP。上传 jar 包用nohup java -jar heart-system.jar --spring.profiles.activeprod 启动。配置反向代理Nginx 把/转发到前端静态文件把/api转发到后端 8080 端口。前端构建产物npm run build生成 dist 目录扔到 Nginx 的 html 目录下。这里有一个关键匹配后端接口的contextPath如果没有特殊配置默认是根路径Nginx 配置类似location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }注意proxy_pass末尾的/它会把/api/user/list改写为后端/user/list。这个细节配错了前端所有接口 404。如果不想做路径改写也可以统一约定后端 controller 的 RequestMapping 以/api开头然后proxy_pass不带斜杠。两种方式选一种保持一致即可。6. 项目文档与常见问题排查6.1 文档应该包含哪些内容这个项目标题里写明了“含文档”我就按自己做项目的习惯说说完整项目文档应该包含哪几块。很多人觉得文档就是照着模板抄一份 README其实远远不够。第一块项目部署文档。数据库脚本执行顺序、application.yml 配置说明、前端 npm install/npm run dev 的启动步骤、生产环境的打包部署命令。这部分的重点是要让一个完全没有接触过这个项目的人照着文档能在 30 分钟内跑起来。第二块接口文档。我直接用 SpringDoc 集成 Swagger 自动生成所有 Controller 加Tag注解VO 字段加Schema(description ...)。启动项目后访问swagger-ui.html可以看到全部接口的请求参数和返回值结构。这个做法比手写 Markdown 表格要高效得多而且代码改了文档不会忘更新。第三块数据库设计说明。每张表的字段含义、枚举值含义、表与表之间的关系。特别是risk_level这种编码字段一定要写清楚 0/1/2 分别代表什么不然后来接手的同事看着一堆数字一脸懵。第四块项目结构说明。后端按 controller/service/mapper/entity/dto/vo 分包前端按 api/components/views/router/store 分包。把每层的职责和调用关系画个文字图描述清楚新人上手会快很多。6.2 从搜索热词看高频问题Vue3失效、端口冲突、驱动版本等我在做这个项目的过程中发现有些问题在搜索热词里反复出现说明是大量同行都会遇到的共性故障。这里挑几个典型问题分享排查链路。问题一Vue3 引入 Element Plus 以后样式失效。这个问题的本质通常是main.js里没有导入样式文件。Element Plus的完整样式需要显式引入import { createApp } from vue; import ElementPlus from element-plus; import element-plus/dist/index.css; import App from ./App.vue; createApp(App).use(ElementPlus).mount(#app);我还遇到过一个变种装了element-plus和element-plus/icons-vue图标组件搞忘了注册页面上一堆图标渲染不出来。问题二后端端口和前端 Vite 开发端口冲突。Vite 默认跑 5173SpringBoot 默认 8080本身不冲突。但如果后端也配了 5173 或前端代理错误就会报Proxy error。我的解决方式是在vite.config.js里配置export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });前端请求/api/xxx就会被代理到后端 8080同时changeOrigin: true让后端收到的 Host 头变成 8080避免跨域限制。问题三MySQL 驱动版本更新后连接报错。这类报错里最常见的是ClassNotFoundException: com.mysql.cj.jdbc.Driver。这是因为 8.x 驱动只认新的类名老的com.mysql.jdbc.Driver在 8.x 已经移除了。检查驱动坐标和连接字符串是否匹配是最直接的排查方向。问题四Vue3 项目里ref的对象拿不到属性或者undefined。我见过最多的场景是直接从后端返回数据赋值给了ref里定义的数组然后页面访问xxxList[0].name报错。核心问题是后端返回的数据结构嵌套层级和前端预期不一致。联调时一定要先用 console.log 打印一遍接口返回值确认数据结构再写渲染逻辑这个习惯能省下大量 debug 时间。问题五MyBatis-Plus 分页插件不生效。如果没有配置 MybatisPlusInterceptor 的分页插件selectPage返回的records可能还是全量数据。配置如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这类问题搜索引擎里经常和“通用 crud 服务”一起出现本质上都是只引入了依赖没有把框架的插件装配到 Spring 容器中导致的。7. 数据安全与系统优化方向7.1 心脏数据不同于普通业务数据医学数据比普通业务数据有着更强的敏感性这套系统存储的是真实的心脏病特征指标虽然不如直接病理报告那么敏感但在项目设计时必须具备私密保护意识。我在系统里做了三层处理第一层接口鉴权。系统集成 Spring Security JWT未登录用户无法请求任何数据接口。登录成功后签发 token前端每次请求在拦截器里带上Authorization: Bearer ${token}。第二层脱敏显示。列表页默认不展示完整姓名只展示“张**”、“李**”这种掩码格式只有点击详情时后端才返回全名。这个功能在 Vue3 前端用一个全局过滤器实现后端接口返回时也做了对应处理。第三层操作日志。通过 AOP 切面记录所有增加、删除、修改请求的操作人和操作时间。一旦有异常操作可以快速追溯到具体用户。7.2 大数据量下的统计优化思路目前系统在模测试数据量在 10 万条以下聚合查询性能完全够用。但如果你要把这套系统推向更大数据量我有三个优化方向供参考。第一是索引设计。heart_disease_record表的常用查询字段应该建立组合索引比如idx_gender_risk (gender, risk_level)、idx_age (age)、idx_create_time (create_time)。多条件筛选时MySQL8.0 会对多个单列索引做 Index Merge但组合索引能减少回表次数速度更稳定。创建索引时考虑每个索引的区分度区分度太低的字段比如 gender单独建索引基本没有意义。第二是聚合查询的预计算。如果统计大屏数据每个小时都要展示可以直接在业务低峰期跑定时任务把聚合结果写入一张dashboard_summary表前端查询时直接读这张摘要表不再实时跑 group by。代价是数据可能延迟一小段时间但对统计展示来说完全可以接受。第三是读写分离。当写入和查询并发都不小的时候主库负责写入从库负责统计查询。MyBatis-Plus 可以通过动态数据源实现多数据源路由。这个方案比较重适合数据量再上一个量级的情况。这次做心脏病数据分析系统有几个技术点让我体会很深。一是选型不要太激进SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 这套组合虽然不算最新但确实是最平衡的选择团队招人容易、官方文档多、出了问题搜得到答案。二是数据分析系统真正的难点不在于算法而在于数据口径和工程落地一张没有设计好字段的表后续每个统计接口可能都要被迫写奇怪的 SQL。三是前后端联调时要尽早统一接口规范和异常码约定等到页面都写完再来改成本至少翻三倍。如果你准备照着这类项目做毕业设计或实际开发我建议先花一天时间把数据库表设计推演清楚再用两天搭建后端骨架前端优先跑通列表和图表页面最后再补权限和统计明细。按这个顺序推进项目大概率不会失控。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号