SpringBoot+Vue 心脏病数据分析系统,这类题目在毕业设计里其实一直不缺关注度。一是医疗健康方向自带加分项,二是前后端分离架构在就业市场上是主流形态,三是数据分析可视化做出来效果直观、答辩好讲。我这些年帮学生评审、调优过不少类似项目,自己也完整搭过几套,今天就把这个项目从选题逻辑、技术选型到前后端实现、部署排错,一条线捋清楚,给准备做毕设或正在学全栈的同学一份可以直接抄作业的参考。
1. 项目选题与整体架构拆解
1.1 为什么"心脏病数据分析"是毕设/课设的好选题
先聊聊选题这件事。很多同学选毕设题目的时候,容易走到两个极端:一是选太普通的"某某信息管理系统",需求就是增删改查,做完自己也觉得没劲,答辩老师问两句深一点的问题就卡壳;二是选太前沿的"基于深度学习的某某预测平台",数据得不到、模型调不通、硬件跑不动,最后把自己坑进去。
心脏病数据分析系统正好落在中间地带。它有三个天然优势:
- 有公开真实数据集可用。Framingham心脏研究数据集是全球公开的经典数据集,包含年龄、性别、静息血压、血清胆固醇、最大心率等十几个特征字段,以及是否患有心脏病的标签列。这意味着你不是在编数据,而是基于真实医疗研究数据做分析和展示,数据来源是站得住脚的。
- 功能层次丰富,可深可浅。基础版是患者数据的增删改查和图表展示;进阶版可以引入相关性分析、数据分布统计;再往上还能做简单的风险预测模型(逻辑回归、决策树等)。功能开发量可以按自己的时间和水平灵活裁剪。
- 可视化效果好,演示价值高。医生问诊场景里,一堆数字没人爱看,但转成年龄分布柱状图、患病率饼图、特征相关性热力图之后,整个系统的专业感立刻就上来了,答辩时的演示效果完全不一样。
这套做下来,你获得的不是"又一届毕设的复制品",而是一个同时覆盖后端API开发、前端交互设计、数据库建模、数据分析可视化的完整全栈项目,简历上也能实实在在写一笔。
1.2 前后端分离架构的核心思路
这套项目采用的是目前业界主流的前后端分离架构。后端用SpringBoot提供RESTful风格API,只负责业务逻辑和数据处理;前端用Vue构建单页应用,只负责页面渲染和用户交互;两者之间通过JSON格式的数据交互,互不干预。
这个架构选择不是跟风,而是因为它切切实实解决了两类问题。
第一,分工清晰。后端开发和前端开发可以完全并行。做毕设虽然大多数时候是你一个人写两端,但模块边界划清楚之后,你的思路会非常清晰——先定义好API接口的URL、请求参数和响应结构,前端和后端各自照着这个"契约"实现,就不容易出现两边代码互相纠缠的混乱状态。我当时带的几个学生,凡是先写接口文档再动手写代码的,联调阶段基本都顺顺利利;上来就闷头写,写到哪算哪的,后期改接口累到想哭。
第二,部署灵活,演示不容易出状况。开发和测试阶段,前端跑在8080端口(Vue默认),后端跑在8081或9090,互不干扰,任何一端崩了都可以单独重启。到了要部署演示的时候,用nginx做一层反向代理,或者直接把Vue打包后的静态文件交给SpringBoot托管,一个端口搞定,演示时不会出现"CORS疯狂报错,现场翻车"的尴尬。
2. 技术栈选型:SpringBoot+Vue+MySQL为什么是黄金组合
2.1 SpringBoot凭什么成为Java后端首选
很多人刚接触Java Web的时候,被Servlet、JSP、SSH、SSM这一堆框架绕得头晕。等到SpringBoot出来以后,整个开发体验完全变了。
SpringBoot的核心价值就一句话:"约定大于配置"。传统SSM项目里,你要写一大堆XML配置文件,配置数据源、配置MyBatis的Mapper扫描、配置事务管理器、配置视图解析器……SpringBoot把这些全部内聚成自动装配机制,你只需要在pom.xml里引入对应的starter依赖,再在application.yml里写明数据库连接串,它就能自动帮你把项目跑起来。
对一个毕设项目来说,SpringBoot带来的效率提升是决定性的。你不需要在"让项目启动"这件事上浪费两天时间,可以把精力全部放在业务功能本身。另外,SpringBoot 2.6.x以上的版本内置了Tomcat,打包成jar之后一个命令就能启动,不再需要单独装一个Tomcat再部署war包,这对接下来的虚拟机演示、远程答辩都友好得多。
工具箱里还有两个高频搭档:Spring Data JPA或MyBatis-Plus。我个人在毕设项目里更推荐MyBatis-Plus,因为它的单表CRUD方法直接开箱即用,你不需要手写基础的insert、update、delete SQL,复杂查询才需要自己写XML或注解SQL。这意味着你的代码量能少一截,出bug的概率也相应降低。
2.2 Vue:适合这个场景的前端方案
前端框架里React和Vue常年被拿来比较,但在毕设和学习场景下,Vue的优势非常明显。
Vue的渐进式设计是它最大的特点——你不需要一开始就掌握完整的工程化体系,可以先写几个简单的组件,然后慢慢引入路由、状态管理。它的模板语法非常接近原生HTML的写法,中文文档质量高,上手门槛在主流框架里是最低的。一个从没写过前端的小白,认真学一周就能写出有模有样的管理后台界面。
配套组件库方面,Element UI(Vue 2)或Element Plus(Vue 3)是首选。这个组件库里的表格、表单、弹窗、分页组件都是企业级后台通用的风格,直接拿过来组装页面,比从零写一套CSS风格统一、交互完备的UI要省太多功夫。做管理平台类项目,Element Plus + ECharts这套组合基本能覆盖你90%的界面需求。
版本选择这里多提醒一句,这个问题我在多个学生项目里踩过坑:如果你是照着网上的旧教程学习,很多教程用的是Vue 2 + Element UI + vue-router 3的组合,这时候不要强行升级到Vue 3,因为Element Plus和旧教程的写法有较大差异,教程里的代码没法直接复制。做毕设的目的是稳定交付,不是追新版本。
2.3 MySQL:小项目里最可靠的数据搭档
数据存储选了MySQL,不是因为别的,就是因为它够用、可靠、资料多。
心脏病患者数据集的特征字段是结构化数据,对事务有要求(比如并发修改患者记录时要保证数据一致),这种场景下关系型数据库天然就是正确答案。MySQL在个人项目和中小型系统里的地位,相当于轿车里的家用经济型——不花哨,但各方面都均衡,怎么开都不会出大问题。
可能有人会问,用Oracle是不是更"高级"?我劝你趁早打消这个念头。Oracle在毕设场景里属于典型的过度设计,安装包几个G起步,配置复杂,而且和SpringBoot的集成资料远不如MySQL丰富。遇到一个问题,百度一搜全是MySQL的解决方案,Oracle的可能搜出来的都是十几年前的论坛帖子。做项目,效率第一,不要给自己加戏。
3. 核心功能模块与数据库设计
3.1 数据库表设计:从分析需求反推表结构
数据库设计是整个系统的基础。表设计得好不好,直接决定后续业务代码写起来顺不顺手。我习惯的做法是:先把系统功能模块列出来,再从每个功能出发反推需要哪些表、哪些字段。
这套系统的核心功能有四块:用户管理、患者数据管理、数据可视化、分析报告。围绕这四块,最核心的数据库表可以设计成这样:
-- 用户表 CREATE TABLE `sys_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '密码(BCrypt加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `role` varchar(20) DEFAULT 'admin' COMMENT '角色', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uni_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';用户表就没什么好说的,标准的用户信息表。注意两点:密码务必用BCrypt加密存储,不要明文存库,这既是安全意识,也方便你在设计文档里写"使用BCrypt对密码进行加密存储"当作安全模块的亮点;用户名一定要加唯一索引,用来支撑登录时的唯一性校验。
接下来是核心的patient表。这个表的字段设计直接参考了Framingham心脏病数据集的统计口径,它包含了评估心血管风险的关键生理指标:
-- 心脏病患者数据表 CREATE TABLE `patient_info` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` varchar(50) NOT NULL COMMENT '患者姓名', `gender` tinyint(1) DEFAULT NULL COMMENT '性别:1男 0女', `age` int(11) DEFAULT NULL COMMENT '年龄', `cp` tinyint(1) DEFAULT NULL COMMENT '胸痛类型(0~3)', `trestbps` int(11) DEFAULT NULL COMMENT '静息血压(mmHg)', `chol` int(11) DEFAULT NULL COMMENT '血清胆固醇(mg/dl)', `fbs` tinyint(1) DEFAULT NULL COMMENT '空腹血糖>120mg/dl(1是 0否)', `restecg` tinyint(1) DEFAULT NULL COMMENT '静息心电图结果(0~2)', `thalach` int(11) DEFAULT NULL COMMENT '最大心率(bpm)', `exang` tinyint(1) DEFAULT NULL COMMENT '运动诱发心绞痛(1是 0否)', `oldpeak` decimal(10,2) DEFAULT NULL COMMENT '运动相对休息ST段压低', `slope` tinyint(1) DEFAULT NULL COMMENT 'ST段峰值斜率(0~2)', `ca` int(11) DEFAULT NULL COMMENT '荧光透视血管数(0~3)', `thal` tinyint(1) DEFAULT NULL COMMENT '地贫类型(1~3)', `target` tinyint(1) DEFAULT NULL COMMENT '是否患病(1是 0否)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='心脏病患者数据表';这些字段如果你自己查过资料就会理解,它们都是心脏病风险预测相关研究中最关键的特征。age是年龄,cp是胸痛类型,trestbps和chol分别反映血压和血脂状况,thalach和exang来自运动负荷测试,oldpeak反映的是心电图ST段的变化幅度。在答辩的时候,如果你能讲清楚每个字段反映的医学含义,老师对你的专业印象分会高很多。
如果你觉得一张表太单薄,可以再加一张查询分析记录表,用来记录用户每次执行的数据筛选分析操作,字段包括筛选条件、影响行数、生成时间等,这就是给系统加了一层"平台属性",比单纯的数据管理显得更完整。
3.2 核心功能模块拆解,以及各自的实现要点
用户认证模块:登录、注册、退出。技术实现上有个选择——用传统的Session会话保持,还是用JWT令牌。我的建议是直接上JWT。原因有几个:前后端分离架构下JWT更契合,无状态会话对服务端压力小;JWT本身就是面试高频话题,你在答辩中能把令牌生成、校验、过期刷新讲清楚,是明显的加分项;而且SpringBoot里集成JWT非常容易,jjwt库只要几行代码就能完成签发和解析。
患者数据管理模块:这是系统的地基,核心操作是患者信息的增删改查、分页查询、模糊搜索筛选。需要注意的不只是CRUD本身,还有筛选条件的组装策略。实际做的时候,患者数据可能上千条甚至更多,全部接口结构上要支持按年龄范围、血压范围、是否患病等多个条件组合查询,这就需要写动态SQL。MyBatis-Plus里有wrapper查询构造器,可以很优雅地解决多条件拼接。加分操作是加一个数据批量导入功能,支持上传CSV文件批量录入患者数据,这在演示时会非常出彩——一张几百条数据的表你鼠标点点点肯定点不完,一键导入就显得这个系统"能扛数据"。
数据可视化模块:这是整个项目视觉上的门面。这部分的内容是整套项目里我最想提醒你重视的部分,因为答辩时老师对界面的直观感受往往比代码逻辑更先入为主。核心图表至少要做四张:年龄段与患病人数的柱状图、患病/未患病占比的环形饼图、各特征与患病关系的横向对比图、特征相关性热力图。图表库选ECharts就行,它提供完整的JavaScript API和详尽的示例,前后端之间只需要约定好数据格式,前端把后端返回的JSON数据填进ECharts的配置项就能出图,做起来非常快。
分析报告模块:可视化是给数据"看相",分析报告是让数据"说话"。这一步不要求你做多高级的机器学习模型,可以基于简单的统计学规则来做。举个例子:系统可以根据患者数据计算年龄均值、血压均值、胆固醇偏高人群占比,生成一段自动化的文本分析报告。进阶的玩法是做一个基于逻辑回归的风险预测接口——用训练好的模型权重,接收前端传过来的患者指标,返回患病概率。这个功能的实现不需要引入复杂的机器学习框架,自己手写LinearRegression的预测公式都能完成,但在项目功能描述里写上"基于逻辑回归模型的风险评估"就会显得很有深度。
4. 从零搭建的实操过程与核心代码实现
4.1 环境与版本选型:别在版本号上栽跟头
版本选型这件事看着不起眼,实际是新手最容易翻车的地方。一个典型的场景:教程里用的是SpringBoot 2.5 + MyBatis-Plus 3.4,你从官网下载了SpringBoot最新版,一启动就报错,去查发现是版本兼容性问题,一查就是一下午。所以这里我给出经过验证的稳定组合:
| 技术组件 | 推荐版本 |
|---|---|
| JDK | 1.8(稳妥)或 17(较新) |
| SpringBoot | 2.6.13(最稳妥,教程资料最多) |
| MyBatis-Plus | 3.5.2 |
| MySQL | 5.7 或 8.0(建议8.0) |
| Node.js | 14.x ~ 18.x(建议16) |
| Vue CLI | 4.x(对应Vue 2)或 5.x(对应Vue 3) |
| Element UI / Plus | 2.15.x(Vue2) / 2.2.x(Vue3) |
这里要多说一句,我看到很多学生项目启动不了,最后原因都是"springboot版本太高"。SpringBoot 3.0是个分水岭,它基于Jakarta EE 9规范,需要JDK 17以上,很多老教程里用的javax.*包名要改成jakarta.*。如果没经验,直接上手3.x版本,网上教程大多对不上号,极其折磨人。做毕设求稳,我推荐直接用SpringBoot 2.6.13 + JDK 1.8这套经典组合,遇到的每一个报错都能在网上找到确切答案。
4.2 后端搭建的五个步骤,以及关键代码
我习惯把后端搭步骤拆成五步,一步做完再走下一步,这样出了问题好定位:
- 在IDEA里新建Spring Initializr工程,填好Group和Artifact坐标,勾选Web、MySQL Driver、MyBatis-Plus依赖,生成工程骨架;
- 配置application.yml数据源和MyBatis-Plus相关参数;
- 使用MyBatis-Plus的代码生成器,根据数据库表自动生成实体类、Mapper接口、Service类;
- 编写统一返回结果类(Result)、异常处理类、工具类;
- 逐模块写Controller层接口。
application.yml里最核心的数据源配置长这样:
server: port: 9090 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/heart_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto几个细节值得展开说一下。url里的serverTimezone=Asia/Shanghai必须配,不然数据库连接会报时区错误;useSSL=false加上,不然高版本MySQL和JDBC驱动之间的SSL校验容易出幺蛾子,这也是热门搜索里"mysql ssl连接错误"的源头。map-underscore-to-camel-case配成true之后,数据库的create_time字段能自动映射到Java实体类的createTime属性,省掉大量手写映射标签的时间。
再来看一个典型的接口代码,比如分页+多条件查询患者数据:
@RestController @RequestMapping("/api/patient") public class PatientController { @Autowired private PatientInfoService patientInfoService; /** * 分页查询患者数据,支持多条件筛选 */ @GetMapping("/page") public Result page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Integer ageStart, @RequestParam(required = false) Integer ageEnd, @RequestParam(required = false) Integer gender, @RequestParam(required = false) Integer target, @RequestParam(required = false) String name) { LambdaQueryWrapper<PatientInfo> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(name), PatientInfo::getName, name) .ge(ageStart != null, PatientInfo::getAge, ageStart) .le(ageEnd != null, PatientInfo::getAge, ageEnd) .eq(gender != null, PatientInfo::getGender, gender) .eq(target != null, PatientInfo::getTarget, target) .orderByDesc(PatientInfo::getCreateTime); Page<PatientInfo> page = patientInfoService.page(new Page<>(pageNum, pageSize), wrapper); return Result.success(page); } }这段代码核心就是LambdaQueryWrapper的链式条件组装。注意它是用lambda方法引用指向实体的getter,编译期就检查字段名,不会因为手写字符串拼错导致SQL语法报错;如果根据条件是否为空决定要不要拼SQL,会照条件是否存在,条件为空就跳过。这个写法在实现查询功能时频繁使用,务必掌握。
前端拿到这个接口的返回结果,结构是固定的:
{ "code": 200, "msg": "操作成功", "data": { "records": [...], "total": 123, "size": 10, "current": 1 } }前端只需要针对这个结构一目了然地渲染表格和分页即可。
4.3 前端搭建:路由、封装、图表实战
前端我建议直接使用Vue CLI创建工程,然后按"C:\项目目录\src\views"这种方式组织页面。每个功能模块对应一个页面目录,比如登录页Login.vue、首页Home.vue、患者管理PatientManage.vue、数据可视化ChartsView.vue,结构清晰查起来也方便。
先是路由配置。一个前后端分离项目,路由是衔接页面跳转的骨架:
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/', component: () => import('@/layout/Layout.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('@/views/Dashboard.vue') }, { path: 'patient', component: () => import('@/views/PatientManage.vue') }, { path: 'analysis', component: () => import('@/views/AnalysisView.vue') } ] } ] const router = new VueRouter({ mode: 'history', routes }) // 全局前置守卫:未登录跳转登录页 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) { next('/login') } else { next() } }) export default router路由守卫这段代码很重要。它实现的是"没登录就不让进系统"的效果,这是每一套管理系统的标配能力。注意跳转逻辑写在beforeEach全局守卫里,是当前页面跳转前统一拦截的,比在页面里分散判断要优雅得多。
然后是对axios做统一封装,主要在请求头携带JWT令牌、统一处理响应码和错误提示:
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器,每个请求自动携带Token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) // 响应拦截器,统一处理后端返回结果 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { Message.error('登录已过期,请重新登录') router.push('/login') } else { Message.error('网络异常,请稍后重试') } return Promise.reject(error) } ) export default request这套封装最大的好处是页面里调用接口时完全不需要关心Token怎么带、错误怎么弹,页面里只需干净利落地写:
const res = await request.get('/patient/page', { params: this.queryParams }) this.tableData = res.data.records this.total = res.data.total最后是可视化页面的核心操作。ECharts的用法可以分为"三步走"——先引入init初始化DOM节点,再写option配置,最后setOption渲染图表。以年龄分布柱状图为例:
import * as echarts from 'echarts' mounted() { this.loadChartData() } async loadChartData() { const res = await request.get('/analysis/age-distribution') const option = { title: { text: '各年龄段患病人数分布' }, tooltip: {}, xAxis: { type: 'category', data: res.data.map(item => item.ageRange) }, yAxis: { type: 'value', name: '人数' }, series: [ { name: '患病人数', type: 'bar', data: res.data.map(item => item.count), itemStyle: { color: '#409EFF' } } ] } const chart = echarts.init(this.$refs.chartRef) chart.setOption(option) }后端返回的数据结构是[{ageRange: '30-40', count: 23}, {ageRange: '40-50', count: 56}]这种列表结构,前端一行map就能整合进图表的data里。图表的美化配置项有很多,比如柱状图加渐变颜色、折线图加面积填充、饼图加环形半径,这些在ECharts官方示例里都有模板,花半小时挑一个改造成自己的配色,整个系统的视觉效果就能提升一大截。
5. 前后端联调与部署:避坑实录
5.1 联调第一步:解决跨域问题
前后端分离开发模式下,最常见的第一道坎就是跨域。报错信息通常是这样的:Access to XMLHttpRequest at 'http://localhost:9090/api/xxx' from origin 'http://localhost:8080' has been blocked by CORS policy。
产生的原因很简单:前端跑在8080端口,后端跑在9090端口,两者不在同一个源(协议、域名、端口任一不同就跨域)。浏览器出于安全策略,默认会拦截这种跨源请求。
解决跨域有两个思路,一个在后端加CORS全局配置,一个在前端开发服务器配代理。
后端方式直观,在SpringBoot里写一个配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }特别注意allowedOriginPatterns这个东西,如果你在SpringBoot 2.7以上版本用allowedOrigins("**")加allowCredentials同时启用,是会被拦下来的,因为这样配置不安全,需要用allowedOriginPatterns替代。
前端方式更推荐,在vue.config.js里配置代理,让前端环境里的接口请求转发到后端:
module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } } }配置好以后,前端请求/api/xxx,devServer会帮你转发到http://localhost:9090/api/xxx,浏览器的请求源是8080,但代理转发发生在服务端,浏览器就感知不到跨域了。
5.2 Vue打包后放进SpringBoot:两种常见部署姿势
开发完成之后,最终部署一般有两种方案市面通用,我都替你们试过。
方案A:用nginx部署前端,反向代理后端接口
正式点的项目普遍用这种。Vue执行npm run build后产出dist目录,把dist丢给nginx托管,nginx配置里把 /api 路径反向代理到后端的9090端口。这种方式下前端和后端是两个独立服务,架构严谨、负载均衡方便,线上真实项目都是这套。
方案B:把dist目录交给SpringBoot托管
毕设演示场景更推荐这种方案,因为最终交付物只是一个jar包,双击就能跑,不用额外装nginx。做法很简单:
把npm run build产出的dist目录里的文件,全部复制到SpringBoot项目的src/main/resources/static目录下,重新打包。SpringBoot会自动把static目录作为静态资源根路径,默认直接访问localhost:9090就能看到前端页面。然后保证前端axios代码里baseURL直接用相对路径'/'或'/api',不需写死域名端口,打包出来的前端资源就能和后端共用一个服务。
这里有一个经典的大坑:Vue路由用history模式,部署后刷新页面404。原因在于history路由依赖浏览器的History API,跳转到某个路径时,是前端路由在接管处理,但一刷新,浏览器会向服务端请求该路径对应的资源,而后端并没有这个路由对应的地址,自然就404了。
解决办法有两个方向。一是在后端加个简单的路由兜底,让所有未知路径都转发到index.html:
@Controller public class PageController { @RequestMapping(value = "/{path:[^\\.]*}") public String forward() { return "forward:/index.html"; } }二是在vue.config.js里把打包output的publicPath配好,同时考虑直接用hash模式路由(url里带#号那种)。hash模式丑是丑点,但它是纯前端路由,永远不涉及服务端路径匹配问题,稳定性最高。演示场景求稳的话,我甚至建议直接用hash模式。
5.3 数据库连接和字符集:从源头杜绝小毛病
数据库这块有两个高频问题我想重点提一下。
第一个是MySQL 8.0的驱动类变化。如果你是跟着旧教程写代码,它引导你配的是com.mysql.jdbc.Driver,但在MySQL 8.0里这个类已经过期了,正确写法是com.mysql.cj.jdbc.Driver。如果项目能编译但启动报驱动类错误,优先检查这里。
第二个是字符集乱码问题。MySQL数据表字符集务必统一用utf8mb4,连接串里也要显式加上characterEncoding=utf8。前后端交互时的中文乱码很多来自这里。如果你导入数据后发现中文变成问号,多半是表结构默认latin1导致的,重建表并指定utf8mb4字符集就好。
6. 常见问题与排查技巧实录
为了让你们少走弯路,我把这几年学生项目里遇到的高频问题梳理成一张速查表。每个问题都是真实场景,排查思路和解决方法是验证过有效的:
| 现象 | 根因 | 排查思路与解决 |
|---|---|---|
| 后端启动报端口被占用 | 9090端口被其他进程占用 | 用netstat -ano|findstr 9090查PID,任务管理器结束后换端口 |
| 前端请求接口报404 | 后端没有对应URL映射 | 核对Controller的@GetMapping路径和前端调用的URL完全一致,注意大小写和斜杠 |
| 前端请求接口报405 | HTTP方法不匹配 | 检查前端用的是GET还是POST,后端接口是@GetMapping还是@PostMapping |
| 登录成功后下次访问又跳登录 | JWT Token没带或过期 | 检查axios请求拦截器是否把Token加到Authorization头,检查后端JWT过期时间设置 |
| 表格日期显示成时间戳 | 后端返回日期格式没处理 | 在application.yml配置jackson的date-format,前端再配合dayjs格式化显示 |
| 数据导入时中文乱码 | CSV文件编码不是UTF-8 | Excel另存时选择UTF-8编码CSV,或用代码强制按UTF-8读取文件 |
| 部署后图表大小变成0 | 容器或图表在隐藏状态渲染 | 在mounted之后使用this.$nextTick渲染图表,或在窗口resize事件中调用chart.resize() |
| 打包后刷新404 | vue-router history模式 | 改用后端路由兜底,或改用hash模式,或配置nginx的try_files |
| MySQL启动后服务连不上 | 服务没启动或端口被改 | Windows打开服务管理器确认MySQL服务状态,确认端口不是3306而是其他 |
| 项目运行慢,首次请求卡顿 | MyBatis-Plus打印SQL日志过多 | 生产环境关闭log-impl配置,本地调试保留,但分页查询检查是否全表扫代替索引 |
排查问题这件事,我劝你别急着改代码。先看报错信息,再看日志,最后定位到具体是前端问题、后端问题还是数据库问题,然后再动手。很多同学一报错就这里改改那里改改,结果越改越乱,最后整个项目都改废了。二分定位法:把请求从前到后划成"浏览器→前端路由→后端Controller→Service→Mapper→MySQL"几段,逐步确认哪一段出了问题,这是最高效的排错方式。
比如前端点查询按钮没反应,先打开浏览器F12看Network面板,确认请求发出去了没有、返回的HTTP状态码是多少、响应体里有没有错误信息。如果请求都没发出去,问题一定在前端代码;请求发出去了但400/500,再往后端日志找原因。这样做一次排错,就能少浪费一两个小时。
最后再分享一个经验
这套项目从选题到落地,我最大的体会是:真正拉开差距的不是技术,而是工程习惯。那些顺利通关的毕业生,往往具备一个共同点——先想清楚再做,先画架构图再写代码,先定接口再联调,每完成一个模块就及时测试。反观那些中途翻车的,几乎都是"打开IDE就写,写哪算哪"。
所以作为参考,你的项目可以这样安排节奏:第一周做需求分析和数据库设计,第二周完成后端基础CRUD和JWT登录,第三周完成前端页面和接口联调,第四周集中做可视化图表、系统测试和部署文档。这样安排,进度压力可控,质量也能保障。剩下的时间可以优化图表颜色、补充测试数据、完善README,每一分投入都能让答辩表现更好。
做毕设其实是在模拟你做真实项目的流程,把上面这些环节都走扎实了,你出去面试时发现的第一个收获会是:这套全栈开发流程,你已经真刀真枪跑通一遍了。