手里拿到一份UCI公开的心脏病数据集,字段不少——年龄、性别、胸痛类型、静息血压、血清胆固醇、最大心率、ST段压低幅度……数据量说大不大,但要在Excel里做多维交叉分析,比如“不同年龄段、不同性别、不同胸痛类型之间的患病比例差异”,透视表拉出来一堆数字,肉眼根本看不出趋势。更要命的是,每次换个角度筛选、分组、汇总,操作流程都得重新来一遍,非常容易出错。与其在表格工具里硬扛,不如直接做一个前后端分离的数据分析管理系统,把数据录入、条件检索、统计图表、可视化管理全部搬到浏览器里。这就是“基于SpringBoot+Vue的心脏病数据分析系统”这个项目的由来。
这套系统前端用Vue,后端用SpringBoot,数据库层通过MyBatis操作MySQL,整条链路都是JavaWeb全栈里边最主流、最容易上手的技术组合。文章会从技术选型逻辑、数据库建模、后端统计接口实现、前端页面与图表对接,一路讲到联调部署阶段踩过的坑,完整还原这套源码从零到跑通的整个过程。如果你正准备做课程设计、毕业设计,或者想通过一个真实项目快速掌握前后端分离项目的完整落地流程,这篇文章和配套源码可以直接作为参考底子。
1. 项目背景与核心功能拆解:为什么数据分析类管理系统值得做成Web应用
1.1 数据分析的痛点:静态表格工具扛不住多维查询
先聊清楚一个核心问题:数据分析类的交互,为什么非要做一个Web管理系统,而不是继续用Excel或者写几段Python脚本?
我自己的判断是:数据清洗和单次统计,Excel、Pandas都做得很好,但一旦进入频繁交互的场景,静态工具的短板就暴露了。比如你想连续查看“40到60岁男性中,不同胸痛类型患者的患病率排序”“女性患者中各年龄段对应的最大心率平均值”这类多条件组合查询,每次换个条件就要重新筛选、重新拉透视表、重新调整图表数据源。操作成本高可以忍,真正怕的是重复操作引入的口径不一致——某个筛选条件少勾选了一项,统计结果就完全不一样了。
把这类功能做成系统之后,查询条件、统计逻辑、图表渲染全部代码化、固化下来,前端页面上每点一次搜索,后端执行的SQL是一致的、可复用、可解释的。这才是数据分析管理系统的真实价值:不是数据本身多复杂,而是让“人机交互分析”这件事变得稳定、可重复、低成本。
1.2 系统功能清单:登录、数据管理、多维统计、可视化
这套系统最终做出来的功能分成四条线:
- 用户登录与鉴权:管理员账号登录,后端登录接口校验用户名密码后返回Token,后续请求携带Token,拦截器统一校验。
- 心脏病患者数据管理:患者的增删改查,支持按年龄区间、性别、胸痛类型、是否患病等多个条件组合筛选。
- 统计分析模块:这是核心。包括各年龄段患病率统计、性别与患病比例、胸痛类型分布、风险指标(静息血压、胆固醇、最大心率)的分组均值统计等。
- 数据可视化:统计分析结果通过ECharts图表展示,包括柱状图、饼图、折线图,与统计接口一一对应。
功能看起来不复杂,但每一块都对应着一个完整的“后端接口+MyBatis SQL+前端页面对接”链路。对初学者来说,刚好是一个既有业务意义、又不至于复杂到失控的项目体量。
1.3 哪些人适合拿这套源码来学习和改造
如果你属于下面几类人群,这套源码和这篇拆解文章的匹配度会很高:
- 正在做课程设计或毕业设计的学生:前后端分离、数据库设计、统计图表、登录鉴权这些点,都是答辩时高频考察的内容。源码逻辑清晰,方便讲清楚设计思路。
- 考虑转行Java全栈的开发新人:SpringBoot+MyBatis后端、Vue前端、MySQL数据存储,一条链路下来,Web开发的核心路径基本都覆盖了。
- 对医疗数据分析场景感兴趣的开发者:数据集本身是公开的、脱敏的数值特征,不含任何真实患者隐私,非常适合用来做学习性质的系统,也方便后续扩展机器学习预测等方向。
2. 技术选型的核心逻辑:SpringBoot + Vue + MySQL + MyBatis为什么是这个组合
很多初学者选技术栈容易犯一个毛病:什么东西火就选什么。实际上,对于一个“管理系统+数据分析场景”的项目来说,选型核心不是追新,而是匹配规模、匹配团队熟悉度、匹配生态完整性。
2.1 后端选型:SpringBoot的成熟生态与低心智负担
后端选择SpringBoot,理由非常朴素。
第一,SpringBoot把Spring生态中繁琐的配置简化了一大截。过去写SSM项目光配置Web.xml、Spring配置文件就要折腾半天,SpringBoot通过自动配置和Starter机制,把绝大多数常用组件的初始化工作都接管了。一个启动类加几个依赖,项目就能跑起来。
第二,SpringBoot的生态足够成熟。MyBatis、MySQL驱动、JWT、FastJson/Jackson这些都有成熟的集成方案,遇到问题社区资料非常丰富,几乎不会卡死在冷门问题上。
第三,对于课程设计和面试场景,SpringBoot是目前Java后端岗位的技术基线,选它意味着项目经验和求职方向一致,答辩或面试时说“我熟悉SpringBoot”是站得住脚的。
2.2 数据库层:MySQL+MyBatis的分工逻辑
MySQL是这个体量项目最自然的选择。它免费、稳定、跨平台、资料多,单机几百万条数据的存储和查询毫无压力,完全覆盖这套系统的数据规模。数据模型上,关系型表结构也特别适合表达“用户表”“患者信息表”这种实体关系。
MyBatis在这个项目里的角色是数据访问层。为什么不直接用Spring Data JPA?两个原因:第一,这套系统的核心是统计分析,SQL里会有大量的GROUP BY、条件聚合、区间统计,MyBatis的XML里写原生SQL非常顺手,优化起来也直白;第二,MyBatis在国内企业级项目中覆盖面很广,学习这一层对就业有实际帮助。
2.3 前端选型:Vue + Element UI + ECharts的实用组合
前端框架选择Vue,主要看重它轻量、渐进式、上手曲线平滑。Element UI提供了一套现成的后台管理风格组件,表格、表单、分页、对话框几乎不用自己造轮子,开发效率非常高。图表部分用了ECharts,它对常见的柱状图、饼图、折线图支持很完善,配置项丰富,H5端和PC端兼容性都很好。
如果硬要比较,React+Ant Design也是一条成熟路径,但从“快速实现、便于讲解、社区资料量大”这个角度,Vue+Element UI对于国内开发者还是更友好一点。
2.4 为什么不选微服务和NoSQL:架构复杂度的边界
也有朋友问过我,为什么不用微服务、不用Redis、不用MongoDB?我的回答一直是:架构必须匹配问题复杂度。这套系统就是一个单体应用,核心是数据管理和统计分析。引入微服务意味着要拆服务、配注册中心、管理分布式事务;引入NoSQL意味着数据一致性维护成本和模型复杂度上升。这些都是没有回报的复杂度,只会让项目难以驾驭、难以解释清楚。
当然,如果你想把项目做得更有亮点,建议在现有单体架构上做纵向扩展,比如后面我会提到的引入预测算法、消息推送、定时统计等,都比盲目上微服务更有性价比。
3. 数据模型先行:心脏病数据集如何设计成MySQL表
3.1 核心需求对数据表的驱动分析
无论功能多复杂,第一步永远是数据建模。我拿到数据集之后,先根据系统功能反推需要哪些表来支撑:
- 用户登录,要有用户表;
- 患者信息管理,要有患者主表;
- 一次就诊的检查指标和诊断结论,要和患者关联记录。
基于这个拆分逻辑,最终设计了以下三张核心表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 系统登录用户 | id, username, password, create_time |
| patient | 患者主表 | id, name, gender, age, create_time |
| patient_health_record | 患者健康检查记录 | id, patient_id, chest_pain_type, rest_blood_pressure, serum_cholesterol, fasting_blood_sugar, resting_ecg, max_heart_rate, exercise_induced_angina, st_depression, heart_disease_flag |
有的朋友习惯把所有字段塞进一张大表,这样写起来简单,但删除、修改某个患者的资料时会导致大量冗余更新。把患者基本信息和个人健康检查记录拆开属于第一范式级别的基本功,既清晰又符合实际业务语义:一个人可以有多次体检记录,历史记录天然保留。
3.2 关键字段的医学口径与统计口径
这部分是很多开发初学者容易忽略的。做数据分析系统,字段不能只关心类型,还要关注字段的取值含义和统计口径。我保留了UCI心脏病数据集里最常用的几个核心指标,字段说明如下:
- chest_pain_type(胸痛类型):取值为0、1、2、3,分别代表典型心绞痛、非典型心绞痛、非心源性疼痛、无症状。后续做饼图统计,需要把数字映射成中文标签再展示。
- rest_blood_pressure(静息血压):单位为mmHg,保留为INT即可。
- serum_cholesterol(血清胆固醇):单位为mg/dl。
- fasting_blood_sugar(空腹血糖):取值为0/1,1表示空腹血糖>120mg/dl。
- max_heart_rate(最大心率):年龄越大通常最大值越低,这就是一个可以交叉分析的点。
- st_depression(ST段压低幅度):DOUBLE类型,是心电图运动负荷试验中的关键指标。
- heart_disease_flag(是否患病):0/1标签,所有统计的最终目标几乎都围绕这个字段展开。
理解字段含义带来的直接好处是写SQL时能明确“我为什么要这么分组、这么过滤”。比如统计“无症状人群中的患病比例”,本质上就是chest_pain_type=3且heart_disease_flag=1的占比,如果不知道3对应无症状,这个分析根本写不出来。
3.3 建表SQL的几个关键细节问题
建表脚本是整套系统的地基,我在实践里反复改过几版,关键细节有这么几个:
第一,密码字段不要用VARCHAR(20),至少留到VARCHAR(64),因为后续加密存储的摘要字符串长度远超过明文长度。
第二,性别字段我用了TINYINT(0/1),比CHAR(2)存“男/女”更省空间,查询时在Java层或SQL层做映射显示。
第三,外键关联别建太多物理外键。patient_health_record虽然逻辑上关联patient表,但物理外键在插入、删除时会带来约束检查开销。业务系统里完全可以用普通索引+Java代码维护逻辑关联。
第四,时间字段统一用DATETIME,避免TIMESTAMP到2038年的边界问题。
核心建表SQL大致长这样:
CREATE TABLE `sys_user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(64) NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `patient` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `gender` tinyint DEFAULT NULL COMMENT '0-女 1-男', `age` int DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `patient_health_record` ( `id` int NOT NULL AUTO_INCREMENT, `patient_id` int NOT NULL, `chest_pain_type` tinyint DEFAULT NULL COMMENT '0-典型心绞痛 1-非典型心绞痛 2-非心源性疼痛 3-无症状', `rest_blood_pressure` int DEFAULT NULL COMMENT '静息血压(mmHg)', `serum_cholesterol` int DEFAULT NULL COMMENT '血清胆固醇(mg/dl)', `fasting_blood_sugar` tinyint DEFAULT NULL COMMENT '1-空腹血糖>120mg/dl', `resting_ecg` tinyint DEFAULT NULL COMMENT '静息心电图结果', `max_heart_rate` int DEFAULT NULL COMMENT '最大心率', `exercise_induced_angina` tinyint DEFAULT NULL COMMENT '1-运动诱发心绞痛', `st_depression` decimal(4,2) DEFAULT NULL COMMENT 'ST段压低幅度', `heart_disease_flag` tinyint DEFAULT NULL COMMENT '1-患病 0-未患病', PRIMARY KEY (`id`), KEY `idx_patient_id` (`patient_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里的核心提醒:整表字符集用utf8mb4,不要用utf8mb3,避免某些生僻字和emoji字符写入报错。另外,所有枚举型字段我都会配上COMMENT,否则三个月后回头看0、1、2、3分别代表什么意思,基本要靠猜。
4. 后端实现:SpringBoot + MyBatis的工程结构与核心编码
4.1 分包结构与统一响应体设计
后端工程结构我是按标准三层架构来的,分包思路如下:
com.example.heart ├── controller // 接口层 ├── service // 业务逻辑层 ├── dao // MyBatis的Mapper接口 ├── entity // 数据库实体类 ├── vo // 接口返回对象 ├── config // 配置类(跨域、拦截器等) ├── common // 统一响应体、异常处理 └── HeartApplication.javaController层只做参数接收和结果包装,Service层放业务逻辑,DAO层只写数据访问接口。这样的分层让整个项目脉络清晰,哪个环节出了问题能很快定位。
统一响应体我用了最经典的结构:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { ... } public static <T> Result<T> error(String message) { ... } }这个封装的核心价值在于:前后端约定一个稳定的数据包格式,前端axios统一根据code判断业务成功与否,而不是每次单独解析后端异常。否则每个接口返回结构都不一样,前端没法写统一的拦截器。
4.2 统计分析SQL的两个大坑:小于号转义与动态条件拼接
这套系统统计功能写起来最需要留神的,恰恰是MyBatis的XML配置。我踩过两个印象深刻的问题,这里完整写出来。
第一个坑:SQL里的小于号必须转义。
统计“低于某年龄段”会用到<符号,但在XML文件里直接写<会报XML解析错误,必须替换成<或者用<![CDATA[ ]]包住。这个报错非常典型:启动时提示“元素类型必须由匹配的结束标签终止”,很多人一头雾水。
第二个坑:动态条件筛选时SQL片段复用和参数命名。
按条件组合查询时,如果每个查询都重写一遍WHERE片段,代码会非常冗长。用MyBatis的<sql>标签定义公共片段再通过<include>引入,能有效减少重复。但注意动态片段里的参数名要和接口方法的@Param注解保持一致,否则会报“There is no getter for property named xxx”。
4.3 分组统计接口的实现示例:按年龄段统计患病比例
下面这段是按年龄段分组的统计SQL,也是系统里最有代表性的一个查询。目标是查出各个年龄段的患病人数、总人数、患病比例,便于前端画柱状图。
<select id="countByAgeGroup" resultType="java.util.Map"> SELECT CASE WHEN age < 40 THEN '40岁以下' WHEN age < 50 THEN '40-49岁' WHEN age < 60 THEN '50-59岁' ELSE '60岁及以上' END AS ageGroup, COUNT(*) AS totalCount, SUM(heart_disease_flag) AS diseaseCount, ROUND(SUM(heart_disease_flag) * 100.0 / COUNT(*), 2) AS diseaseRate FROM patient_health_record GROUP BY ageGroup ORDER BY totalCount DESC </select>三个细节值得展开:
<转义不可或缺,我已经强调过;- SUM(heart_disease_flag)的写法利用MySQL中布尔值参与数值计算的特性,一行代码算出患病人数,避免子查询;
- 用ROUND保留两位小数,前端展示百分比时不会出现一串小数尾巴。
对应的Mapper接口方法:
List<Map<String, Object>> countByAgeGroup();可能有人会问,为什么返回类型直接写Map而不是定义一个VO类?原因是分组统计的字段组合和执行阶段才确定,强类型VO反而僵硬。但需要注意,外层拿到的是Map,key的命名完全依赖SQL里定义的别名,所以别名一定要规范,并且前后端联调时明确约定字段名。
类似地,按胸痛类型统计分布的SQL如下:
<select id="countByChestPainType" resultType="java.util.Map"> SELECT chest_pain_type AS chestPainType, COUNT(*) AS count FROM patient_health_record GROUP BY chest_pain_type ORDER BY count DESC </select>前端拿到chestPainType后,需要做一次数字到中文标签的映射,我会在后面的前端章节说明。
4.4 后端接口层的设计与登录鉴权
Controller层写起来比较直接,以统计接口为例:
@RestController @RequestMapping("/api/stats") public class StatsController { @Autowired private StatsService statsService; @GetMapping("/ageGroup") public Result<List<Map<String, Object>>> ageGroupStats() { return Result.success(statsService.countByAgeGroup()); } @GetMapping("/chestPain") public Result<List<Map<String, Object>>> chestPainStats() { return Result.success(statsService.countByChestPainType()); } }登录鉴权部分,我没有引入Spring Security整套框架,而是用JWT加拦截器的轻量方案。核心逻辑:
- 登录时校验用户名密码,成功后生成JWT返回给前端;
- 后端定义一个拦截器,拦截
/api/**下的请求,校验请求头里的Token; - Token合法才放行,不合法直接返回401;
- 前端路由守卫根据登录状态和Token决定是否跳转登录页。
这套方案的好处是代码量小,核心逻辑容易讲清楚,非常适合课程设计答辩。如果项目想加强安全强度,后期再引入Spring Security也完全兼容。
4.5 密码加密:不容忽视的基本功
这里必须单独提醒:数据库里的用户密码一定不能存明文。我用的是BCrypt加密,Spring Security Crypto工具包里就能找到,使用成本很低:
String encoded = new BCryptPasswordEncoder().encode(rawPassword); boolean matches = new BCryptPasswordEncoder().matches(rawPassword, encoded);很多初学项目会直接用MD5加盐,但BCrypt的正确率更可靠,它内部自带随机盐,每次加密结果不同,防彩虹表效果更好,而且验证明文密码时不需要自己管理盐值,对开发者非常友好。
5. 前端Vue:路由、页面、图表与后端接口的完整对接
5.1 前端页面结构与路由配置
前端用的是Vue加Vue Router加Axios,UI组件库Element UI。整个前端项目结构如下:
src ├── router/index.js // 路由配置 ├── views/Login.vue // 登录页 ├── views/Layout.vue // 主布局,包含侧边导航 ├── views/PatientList.vue // 患者数据管理 ├── views/StatsDashboard.vue // 数据统计可视化 ├── api/ // 接口封装 ├── utils/request.js // axios 实例 ├── App.vue └── main.js路由配置时用了一个小技巧:登录页独立路由,登录后进入的主布局路由统一嵌套在一个父路由下,子路由分别对应患者管理、统计看板等页面。这样侧边导航只需要维护一份菜单配置,切换页面时整体布局不会重新加载。
const routes = [ { path: '/login', component: Login }, { path: '/', component: Layout, redirect: '/dashboard', children: [ { path: 'dashboard', component: StatsDashboard }, { path: 'patient', component: PatientList } ] } ];进入主页面之前需要加一道导航守卫:如果本地没有Token,强制回到登录页。这是前后端分离项目里必须处理的环节,否则用户直接刷新页面就会被踢回登录或者看到一堆报错。
5.2 axios封装与登录态携带
axios请求的封装是整个前端工程的地基。我在这里做了三件事:
- 设置基础URL为后端服务地址,便于统一切换环境;
- 请求拦截器自动携带Token到请求头;
- 响应拦截器统一处理后端返回的业务码,业务失败时弹出提示,HTTP状态码401时清除登录态并跳转登录页。
核心代码如下:
const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; }); service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { Message.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res.data; }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } Message.error(error.message); return Promise.reject(error); } );写到这里我突然觉得有个细节很重要:统一封装后,页面里拿到的直接是业务数据,不用每次写res.data.data这种嵌套取值的代码。保持这个约定,后面所有页面里取数据都会很清爽。
5.3 统计看板的ECharts对接实现
统计看板是整个前端的核心页面。ECharts的完整对接链路是:组件挂载后调用统计接口,拿到后端返回的数组,把数组转换成ECharts需要的data结构,再调用setOption渲染图表。需要考虑的一个边界问题是,组件重新挂载时要先销毁旧图表实例,防止重复初始化内存泄漏。
下面这段是按年龄分组统计的完整实现:
async function loadAgeStats() { const data = await ageGroupStats(); const chart = echarts.init(document.getElementById('ageChart')); chart.setOption({ tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.map(item => item.ageGroup) }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.map(item => item.diseaseRate), name: '患病率(%)' }] }); }胸痛类型分布是饼图,重点是中文标签映射。我用一个映射对象转换:
const chestPainLabels = ['典型心绞痛', '非典型心绞痛', '非心源性疼痛', '无症状']; const pieData = data.map(item => ({ name: chestPainLabels[item.chestPainType], value: item.count }));这类映射逻辑放在前端做还是后端做,我的原则是:展示性的标签映射放前端,口径性的统计逻辑放后端。胸痛类型数字对应的中文名是展示层需求,前端处理更灵活;而表名、字段、分组逻辑、过滤条件这种核心口径不能在前端暴露,否则换一个前端壳,统计口径就会失控。
6. 联调与部署阶段的坑:跨域、时区、编码、版本冲突全记录
真正把前后端联调起来的时候,遇到的问题比写功能的时候多得多。这一章直接按坑清单的方式写,每个坑都有现象、原因、解决方案,方便你排查时对号入座。
6.1 跨域:CORS配置的三种方案
前后端分离后第一个拦路虎一定是跨域。前端跑在8080端口,后端跑在8081端口,浏览器会判定端口不同导致跨域,请求发出去但响应被拦截,控制台报错。经验做法有三种:
- 后端配置CORS过滤器;
- 前端开发环境用Vue CLI的
devServer.proxy代理转发请求; - 部署后通过Nginx反向代理统一同源。
我开发阶段用的是后端CORS配置。写一个WebMvcConfigurer配置类,映射允许跨域的路径、请求头和请求方法,代码如下:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这里特别说明一个细节:如果定义了拦截器,OPTIONS预检请求也要放行,否则浏览器预检请求会在拦截器就被拦截掉,前端看到的还是跨域错误。这是非常隐蔽的坑,我在第一次联调时卡了大半天。
6.2 MySQL连接参数:SSL、时区、编码三连坑
MySQL数据库连接串是配置出错的重灾区,我有三个参数必须写对的结论:
url: jdbc:mysql://localhost:3306/heart_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=trueuseSSL=false:本地开发环境下,不关闭SSL会提示“Establishing SSL connection without server's identity verification is not recommended”,并且某些MySQL版本下会尝试加密连接导致握手失败;serverTimezone=Asia/Shanghai:5.x驱动读取数据库时间时默认取JVM时区,不设置会导致时间字段偏移8小时,数据写入和查询隔了时区;characterEncoding=utf8:防止中文乱码。
驱动版本方面,MySQL 8.x要选com.mysql.cj.jdbc.Driver,MySQL 5.x则用com.mysql.jdbc.Driver。这两个驱动类名一旦写错,启动时直接报找不到驱动类错误。
6.3 SpringBoot版本太高导致MyBatis自动配置失效
这个坑很有意思。SpringBoot 3.x发布后,很多刚开始接触项目的同学直接选了最新版本,结果发现引入mybatis-spring-boot-starter后始终无法注入Mapper。原因是SpringBoot 3.x基于Jakarta EE,而传统MyBatis Starter的配置路径发生了变化,部分老版本依赖会静默失效。面对这种情况,我建议:
- 如果是课程设计或学习项目,不必追求最新,用SpringBoot 2.7.x配合MyBatis即可;
- 如果一定要用SpringBoot 3.x,需要切换到
mybatis-spring-boot-starter的3.x版本,并确认数据库驱动、JWT等所有上游依赖都兼容Jakarta命名空间。
这类版本兼容问题排查起来比较耗时,最好的办法就是在项目初期锁定技术版本清单,别让依赖版本变成变量。
6.4 前端日期显示与后端日期序列化不一致
患者创建时间字段在联调时出现过一次显示异常:后端返回的时间戳是2024-05-30 12:00:00,前端显示却变成2024-05-30T12:00:00。原因是Jackson对LocalDateTime的默认序列化格式带有T分隔符。
解决方式有几种,最简单的是在配置文件里指定全局日期格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8更推荐的做法是在实体类的时间字段上使用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")注解做显式声明。显式注解的好处是格式清晰可控,不会因为全局配置变更影响局部字段。
6.5 Vue打包后放进SpringBoot部署的静态资源路径问题
最后一个部署相关的坑:如果想把前端打包后的dist目录放到SpringBoot的src/main/resources/static下面做成一个单体部署,需要注意前端路由模式。
Vue路由如果用的是HTML5的history模式,打包后刷新页面会出现404,因为后端没有对应的路由转发规则;而用hash模式则不会出现这个问题。所以单体部署时最省心的方案是使用hash路由模式,并且确认前端build后的资源路径是从根路径加载的,否则CSS/JS引用路径错乱。
如果项目最终保持前后端分离部署,前端用Nginx托管,后端单独跑在某个端口,那么跨域配置和静态资源这两个问题都能绕开,反而更干净。
7. 本地从零跑通全流程:环境检测、建库、启动、验证四步走
7.1 准备环境:版本组合建议
如果你手上没有现成环境,我建议按下面这组稳定版本来,都是经过大量项目验证的:
| 组件 | 建议版本 | 备注 |
|---|---|---|
| JDK | 1.8 或 11 | 17也能跑,但需确认依赖都兼容 |
| Maven | 3.6.3+ | 项目级依赖管理 |
| MySQL | 5.7 或 8.0 | 注意驱动类名差异 |
| Node.js | 14+ | 前端构建环境 |
| 后端框架 | SpringBoot 2.7.x | 对应MyBatis starter 2.x |
| 前端框架 | Vue 2.x + Element UI | 配套ECharts库 |
新手最容易出问题的是JDK和SpringBoot版本不匹配,建议直接按这个清单来,减少变量。
7.2 创建数据库并初始化
先建立数据库,然后执行建表脚本,再插入管理员账号和一部分演示数据。
mysql -u root -p -e "CREATE DATABASE heart_db DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p heart_db < heart_db.sql如果手边没有现成演示数据,可以把UCI心脏病数据集CSV通过工具导入到patient_health_record表,注意CSV里的整数/浮点类型要和表字段对应。导入完成后可以执行一条简单的验证SQL,比如统计总记录数,确认数据没有错位:
SELECT COUNT(*) FROM patient_health_record;7.3 修改配置并启动后端
找到后端工程的application.yml,修改数据库相关配置:
spring: datasource: url: jdbc:mysql://localhost:3306/heart_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=true username: root password: your_password server: port: 8081然后执行Maven命令启动:
mvn clean package -DskipTests java -jar target/heart-system.jar启动后看到“Started HeartApplication in x.x seconds”就说明后端就绪了。可以用浏览器直接访问接口的Swagger或手动请求一个API接口验证。
7.4 安装前端依赖并启动
前端项目根目录下依次执行:
npm install npm run serve如果网络环境导致npm安装慢,可以换成国内镜像源,但注意镜像源的版本同步可能存在短暂延迟。启动后访问Vue CLI提示的本地地址,默认一般是http://localhost:8080。
登录后进入统计分析页面,看图表能否正常加载,再进入患者管理页面做一次增删改查操作,整个流程走通就基本没问题了。
7.5 我实际跑通后的一些个人体会
最后聊几句个人体会。
这套系统我前后改了三个版本,最大的感触是:数据分析类管理系统,统计口径和SQL的正确性比页面效果重要得多。页面好坏是视觉层面的事,而统计结果一旦口径错了,整个系统就失去了说服力。所以拿到数据第一步,一定要花时间理解每个字段的业务含义,再开始建表和写SQL。
另一个体会是,前后端分离项目联调阶段暴露的问题,往往是配置问题多于代码逻辑问题。跨域、时区、编码、依赖版本,这些坑每一个看起来都很小,但遇到一个就能卡几个小时。所以项目早期就把配置规范定好、版本锁定好,后面的联调会顺畅很多。如果让我重做一遍,我会先把生产环境部署方案定下来,再决定路由模式和跨域策略,而不是开发到最后才考虑部署。这套源码本身不算复杂,但它是完整走通“需求分析-数据建模-后端开发-前端对接-部署验证”全流程的样板,拿它当底子,替换成其他业务场景的数据集,就能扩展出新的管理系统项目。