
每年毕业季后台都会收到类似的私信学长SpringBootVue的毕设题到底怎么做才不像应付差事今天借社区老人健康信息管理系统这个题目把从选题拆解、数据库设计、前后端编码到最后的部署联调完整捋一遍。这个题不只是个查档案的CRUD它涉及多角色权限、健康数据录入、异常预警、统计报表、家属互通真正做出来是一个能落地的小型业务系统。适合用来做毕业设计、课程设计也能给想入门前后端分离开发的朋友当练手项目。先说结论SpringBootVueMySQL这套组合做老人健康管理属于既稳又能出彩的选项。后端业务边界清晰前端交互和可视化表现力强数据库层面又能承载业务关系。只要把关键设计想清楚代码量其实不大但能讲的东西非常多——这正是答辩时最需要的。1. 这个毕设题到底在做什么1.1 选题背景与价值判断社区老人健康管理系统本质上是把线下的纸质健康档案和服务流程搬到线上。老人基本信息、血压血糖心率体温、体检记录、用药计划、家属联系方式这些数据散落在纸面上很难查更别说做长期趋势分析了。系统要解决的问题就是统一建档、实时记录、异常提醒、数据可视化。选这个题有天然优势需求明确、场景贴近生活、功能边界清晰评委一听就知道这个系统是干什么的不需要费劲解释。同时它又有足够的复杂度。多角色管理员、社区专员、家属的权限区分健康记录的批量录入预警规则的设定趋势图的展示这些模块每一个都有深入挖掘的空间工作量能控制在一个学期内也能适当往上加码。1.2 三个用户角色与核心功能闭环我在梳理需求的时候没有上来就列功能清单而是先想清楚谁会使用这个系统、他们分别关心什么。管理员和社区专员是核心使用方负责老人建档、健康记录录入、体检信息维护、用药计划和社区公告管理。家属是重要的外延用户登录后可以查看自家老人的健康数据、接收异常预警消息。老人的直接登录使用场景相对较轻通常由代录或设备自动采集所以老人端可以做得轻量甚至只作为档案对象存在。这里有一个设计判断要不要做老人本人登录我最后做的是老人表独立于用户表老人本人可以绑定一个登录账号但在第一版里只提供查询和报告下载功能。原因很简单——老人端的高频操作是数据录入让老人自己敲键盘录血压不太现实通常是设备测量后自动上报或者社区专员帮忙录入所以他的操作面板复杂度不需要和家属端一样。核心功能闭环是这样的社区专员为老人建档然后按天录入健康数据血压、血糖、心率、体温等系统根据预设阈值自动标记异常项并生成预警消息推送给绑定的家属账号。家属通过趋势图查看一段时间的数据变化还可以下载体检报告PDF。管理员则负责账号分配、数据统计和社区公告维护。这个闭环覆盖了一个健康数据从产生、采集、分析到反馈的完整链路不管是写论文还是做答辩都能讲出一套逻辑来。2. 技术选型SpringBootVueMySQL的组合逻辑2.1 后端SpringBoot接得住业务的稳定性经常有同学问用SSHSpringMVCHibernate行不行用传统的SSM行不行技术上都可以但现在做新项目SpringBoot的收益明显更高。SpringBoot最核心的价值是自动配置和起步依赖。传统SSM项目里配置文件要写数据源、事务管理器、MyBatis映射、SpringMVC视图解析器任何一个环节配错启动直接报错。SpringBoot通过starter机制把常用的依赖组合好约定大于配置一个application.yml就能把数据源、端口、日志全部搞定。这对毕设这种开发周期短、团队可能只有自己一个人的项目来说等于省掉了大量和环境搏斗的时间把精力留给业务逻辑。另外一个实际优势是第三方生态。做权限有Spring Security或者Shiro做文档有Knife4j做数据库操作有MyBatis-Plus。这些都是社区沉淀多年的解决方案遇到问题一搜就有答案不至于卡在某个jar包折腾两周。2.2 前端Vue的渐进式开发体验Vue在这套系统里的角色是构建单页应用SPA核心价值在于组件化和响应式数据绑定。组件化就很好理解——老人列表是一个组件健康表单是另一个组件趋势图是一个图表组件。每个组件只管自己的部分代码结构清晰到时候答辩讲起来也方便先把页面拆成组件树然后逐个说明数据如何流转。响应式数据绑定则是Vue的灵魂数据变化自动更新DOM不需要手动操作页面元素开发效率比直接操作DOM高很多。Vue3相比Vue2组合式APIComposition API让逻辑复用更加方便。比如获取老人列表以前可能需要mixins现在一个useLoadOldPerson()函数就能搞定然后把加载状态、列表数据、异常信息都封装在里面。配合Vite构建工具启动和热更新都很快开发体验比老一代构建工具顺手得多。2.3 数据库MySQL的成熟与稳妥MySQL是这个组合里的老三样但它依然是这种中小型业务系统最稳妥的选择。顾虑完全不必有多表关联查询、事务、索引、排序这些功能MySQL都支持得很好。还有一个很现实的理由MySQL的安装教程、配置教程、面试题在网上浩如烟海出现问题基本都能找到解决方案。比如MySQL 5.7和8.0的驱动差异、SSL连接错误、时区问题这些典型坑都有现成的排查经验。用MySQL还能方便你在毕业论文里写数据库设计这一章——ER图关系模式范式分析这些内容都是MySQL系的常规素材。2.4 三者联动一次完整请求的生命周期为了把技术栈串起来我通常会画这样一条线当然这是用文字描述不是流程图工具用户在Vue页面上点击查询老人列表Vue组件调用封装好的Axios请求发送HTTP请求到SpringBoot的ControllerController接收参数调用Service层处理业务逻辑Service层调用Mapper层MyBatis-Plus操作MySQL数据库数据返回经过统一Result封装前端解析后通过响应式数据渲染到页面这一步最好在答辩前对着代码逐行讲一遍能讲清楚从页面到数据库再回到页面的同学技术分数一般都不会低。另外有一个容易忽视的点跨域问题。开发时Vue在3000端口SpringBoot在8080端口浏览器默认禁止跨端口访问需要在前端配置代理vue.config.js里的devServer.proxy或者后端开启CORS。我推荐开发阶段用代理生产环境用Nginx转发不要把CORS当成常规方案——纯后端放开跨域虽然开发方便但生产环境会有安全风险。3. 数据库设计每一张表都要有存在理由3.1 核心表结构设计数据库设计是整个系统真正的地基表设计不合理后面所有代码都是空中楼阁。我习惯先设计表再写代码避免边写代码边改表的痛苦。这个系统的核心表一共有6张系统用户表、老人基本信息表、健康记录表、体检记录表、用药计划表、公告表。系统用户表CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, real_name varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, role tinyint NOT NULL DEFAULT 2 COMMENT 1管理员 2社区专员 3家属, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT系统用户表;密码字段不存明文至少要用MD5加盐或者BCrypt处理一次这是论文里能写的安全点。角色字段我用数字表示权限控制时直接判断数字大小比字符串比较更可靠。老人基本信息表CREATE TABLE old_person ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, gender tinyint NOT NULL, birth_date date DEFAULT NULL, id_card varchar(18) DEFAULT NULL, phone varchar(20) DEFAULT NULL, address varchar(200) DEFAULT NULL, emergency_contact varchar(50) DEFAULT NULL, emergency_phone varchar(20) DEFAULT NULL, chronic_disease varchar(200) DEFAULT NULL COMMENT 慢性病史, medication_status varchar(200) DEFAULT NULL COMMENT 长期服药情况, avatar varchar(200) DEFAULT NULL COMMENT 头像路径, status tinyint DEFAULT 1, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT老人基本信息表;设计时要注意一个原则老人表不存血压、血糖这种动态数据只存相对固定的个人信息。曾经有同学把最近一次血压直接放进老人表结果每次更新都要UPDATE一条记录历史数据完全丢失这样没办法画趋势图也很容易丢失数据。动态数据应该单独建表用外键关联老人ID。健康记录表是这个系统的核心表它的设计有讲究CREATE TABLE health_record ( id bigint NOT NULL AUTO_INCREMENT, old_person_id bigint NOT NULL, type tinyint NOT NULL COMMENT 1血压 2血糖 3心率 4体温, value decimal(8,2) NOT NULL, unit varchar(20) DEFAULT NULL, record_time datetime NOT NULL, remark varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_old_person_time (old_person_id, record_time) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT健康记录表;这里我采用的是通用型设计——用type字段区分记录类型value存数值。这么做的好处是以后加一个新的健康指标比如血氧饱和度不需要改表结构加一个枚举值就行。缺点是每个指标的单位和正常范围维护起来要靠另一张配置表或者代码里定义。还有一种字段化设计建表时就把收缩压、舒张压、血糖、心率各做成一个字段。两种设计我都用过通用型适合逐步扩展字段型适合指标确定不变化的场景。这个毕设里指标会不断增加所以我选了通用型。注意还有一个细节健康记录表一定要建(old_person_id, record_time)的联合索引。如果没有这个索引按老人加时间范围查询数据时MySQL会直接全表扫描数据量上到几万条之后响应时间会明显变慢。答辩时如果你说出我建了联合索引来优化慢查询技术功底马上就能体现出来。体检记录表用来存周期性的体检报告信息CREATE TABLE health_check ( id bigint NOT NULL AUTO_INCREMENT, old_person_id bigint NOT NULL, check_date date NOT NULL, check_org varchar(100) DEFAULT NULL COMMENT 体检机构, summary varchar(500) DEFAULT NULL COMMENT 体检结论, report_file varchar(200) DEFAULT NULL COMMENT 报告文件路径, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_old_person (old_person_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT体检记录表;report_file字段保存的是上传文件的路径文件本身放在服务器本地目录或者对象存储里。这里有个避坑项不要把文件转成Base64字符串直接存数据库非常占用空间性能也差应该只在数据库存路径。3.2 表间关系与事务意识六张表之间的关系并不复杂老人表是核心健康记录、体检记录、用药计划都关联到老人表家属通过用户表关联到老人表需要一张家属与老人的关联表。CREATE TABLE family_bind ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 家属用户ID, old_person_id bigint NOT NULL, relation varchar(20) DEFAULT NULL COMMENT 父子/母子等, bind_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_old (user_id, old_person_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT家属绑定表;一个家属可以绑定多位老人一位老人也可以有多个家属这个多对多关系需要通过关联表来实现。在新增老人之后立即给家属发送绑定邀请时就需要用事务来保证先插入old_person再插入family_bind两个操作要么同时成功要么同时失败。SpringBoot中使用Transactional注解就可以解决。3.3 数据库版本与连接配置MySQL这边有两个容易踩的坑提前说清楚。MySQL 8.0开始JDBC驱动类名变成了com.mysql.cj.jdbc.Driver如果你还在用5.x时代的老配置会直接报错。另一个是连接串需要加上时区和SSL配置否则可能出现时区错误、SSL连接错误spring: datasource: url: jdbc:mysql://localhost:3306/health?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.DriverallowPublicKeyRetrievaltrue这个参数是MySQL 8.0在本地连接时经常需要的不加可能报Public Key Retrieval is not allowed。这里建议把数据库字符集统一成utf8mb4不要用utf8——utf8mb4才是完整的UTF-8编码能正常存储emoji、生僻字等四字节字符。老人名字里如果带了很生僻的字用utf8可能直接报Data truncation错误到时候排查起来非常痛苦。4. 后端核心实现SpringBoot里的业务落地点4.1 项目初始化与基础配置创建SpringBoot项目我推荐直接在IDEA里用Spring Initializr选择Java 8或者Java 11都可以SpringBoot版本建议2.7.x稳定版本即可。很多同学喜欢一上来就选最新的3.x版本结果发现一些老教程里的写法都变了那些教程大多基于SpringBoot 2.x。安全稳定的选择是2.7.x版本它的社区资料最丰富遇到问题解决起来也最快。pom.xml里需要引入的核心依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.25/version /dependencyMyBatis-Plus这个选型值得解释一下。它省去了写XML、写BaseMapper接口的工作量单表CRUD直接继承内置接口就能做。对于这个系统的绝大部分操作——按ID查老人、分页查询健康记录、按条件筛选——都能直接用代码量能省30%以上。4.2 统一返回与JWT认证前后端分离的项目接口返回格式必须统一。我习惯的返回结构是{ code: 200, message: 操作成功, data: { ... } }为了达到这个效果写一个统一的Result类Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message 操作成功; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }统一返回的好处是前端只需要做一个拦截器处理三个状态——成功、业务失败、未登录不需要每个接口单独写解析逻辑。登录认证我强烈推荐JWT方案原因只有一个它是无状态认证后端不用保存Session对前后端分离和多端部署很友好。核心逻辑是用户登录成功后服务端签发一个token返回给前端前端每次请求都把这token放在请求头里后端拦截器校验token合法性。JWT工具类Component public class JwtUtil { private static final String SECRET health-community-system-secret; public String createToken(Long userId, Integer role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }拦截器Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(未登录); } // 校验token解析失败则抛异常 Claims claims jwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } }在WebMvcConfig注册时要把登录接口和静态资源放行Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register); } }我踩过一个坑前端请求会先发一个OPTIONS预检请求如果不放行这个请求拦截器会直接拦下来导致跨域失败一定要记得把OPTIONS方法放行掉。4.3 老人档案CRUD与文件上传老人档案模块就是典型的单表CRUD用MyBatis-Plus写起来非常简单。Service public class OldPersonServiceImpl extends ServiceImplOldPersonMapper, OldPerson implements OldPersonService { Override public PageOldPerson pageOldPerson(int page, int size, String keyword) { LambdaQueryWrapperOldPerson wrapper new LambdaQueryWrapper(); wrapper.and(StringUtils.isNotBlank(keyword), w - w.like(OldPerson::getName, keyword) .or().like(OldPerson::getPhone, keyword) ); return this.page(new Page(page, size), wrapper); } }这里有个细节值得讲用LambdaQueryWrapper而不是普通的QueryWrapper最大好处是类型安全实体字段改名后编译期就能发现错误而不是运行期才爆出SQL异常。文件上传比如老人头像、体检报告用的是SpringBoot的MultipartFilePostMapping(/api/upload) public ResultString upload(RequestParam(file) MultipartFile file) { // 生成唯一文件名防止重名覆盖 String originalFilename file.getOriginalFilename(); String ext originalFilename ! null ? originalFilename.substring(originalFilename.lastIndexOf(.)) : ; String fileName UUID.randomUUID() ext; // 按照日期分目录存储避免一个目录文件过多 String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); String realPath D:/upload/ datePath /; File dir new File(realPath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(realPath fileName)); return Result.success(/upload/ datePath / fileName); }注意文件路径不要直接存在根目录建议用年月日建子目录。体检报告如果是PDF前端可以用pdf.js或者浏览器内置的iframe src来预览图片则可以直接用img标签展示。之前有同学问Vue的image能显示PDF吗严格说image标签不能直接显示PDF要么用embed、iframe要么用PDF.js渲染成canvas再显示。我在这个项目里体检报告用的是iframe展示简单够用。4.4 健康记录上报与异常预警健康记录上报是核心接口。社区专员每次测完血压、血糖后把数据提交到后端PostMapping(/api/health-record) Transactional(rollbackFor Exception.class) public Result? addHealthRecord(RequestBody HealthRecord record) { // 1. 保存健康记录 healthRecordService.save(record); // 2. 查询阈值配置血压高于140/90属于异常 Threshold threshold thresholdService.getThresholdByType(record.getType()); // 3. 如果异常生成预警消息通知家属 if (threshold ! null isAbnormal(record, threshold)) { ListFamilyBind binds familyBindService.getByOldPersonId(record.getOldPersonId()); for (FamilyBind bind : binds) { warningService.createWarning( record.getOldPersonId(), bind.getUserId(), 健康异常提醒 getTypeName(record.getType()) 数值为 record.getValue() ); } } return Result.success(null); }这段代码有几个要点。Transactional保证记录保存和预警生成的原子性——如果预警消息生成失败健康记录也要回滚不会出现数据存了但家属没收到提醒的脏状态。异常判断我用的是阈值配置而不是硬编码这样调整阈值不用改代码是很小但很实用的设计。定时任务可以做用药提醒。健康管理系统里老人的长期用药很重要可以做一个定时任务每天定时查用药计划表给相关人员发送提醒Component public class MedicationTask { Scheduled(cron 0 30 8 * * ?) public void sendMedicationReminder() { // 查询当天需要用药的老人及其家属绑定 // 发送短信或站内消息提醒 } }在启动类加上EnableScheduling开启定时即可。注意定时任务里的逻辑最好是幂等的——重复执行不会产生重复数据。最简单做法是每次执行前查一下今天的提醒是否已经发过发过就跳过。5. 前端核心实现Vue3里的界面与交互5.1 项目初始化与路由配置前端我建议直接使用Vue3 Vite Element Plus的组合。Vite的启动速度比vue-cli快很多Element Plus的组件库能直接满足后台管理的多数场景表格、表单、弹窗、树形控件都有现成的。用Vite创建项目npm create vitelatest health-web -- --template vue cd health-web npm install npm install element-plus axios vue-router pinia echarts路由配置使用vue-router 4这里要注意初始路由和登录页的处理import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: Login, meta: { public: true } }, { path: /, component: HomeLayout, children: [ { path: , redirect: /dashboard }, { path: dashboard, component: Dashboard }, { path: old-person, component: OldPersonList }, { path: health-record, component: HealthRecord }, { path: statistics, component: Statistics } ] } ] const router createRouter({ history: createWebHistory(), routes }) // 路由守卫没有token就跳登录 router.beforeEach((to) { const token localStorage.getItem(token) if (!to.meta.public !token) { return /login } return true })路由守卫是前端权限的第一道门。它的作用是未登录用户不允许访问后台页面但真正的数据权限还是靠后端接口拦截器控制。前端守卫负责体验后端拦截器负责安全这个边界要分清楚。Vue3里还想提一个动态路由的概念。简单说就是根据用户角色动态添加路由表管理员能访问用户管理家属用户不能。这个系统的第一版可以用固定的路由表加按钮级权限控制实现例如v-ifrole1控制某个菜单显示效果一样复杂度低很多。如果有余力再上动态路由不迟。5.2 Axios封装与开发代理Axios封装是我在所有Vue项目里必做的事不封装直接用的后果是每个页面都要重复写loading、错误提示、token加头代码能重复到让人崩溃。我习惯把axios实例单独放在src/utils/request.jsimport axios from axios import { ElMessage } from element-plus 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 token } return config }) // 响应拦截器统一处理错误 request.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 { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(请求失败请稍后重试) } return Promise.reject(error) } ) export default request开发时的跨域通过vue.config.js代理解决// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端的/api/old-person请求会被代理到后端的http://localhost:8080/api/old-person浏览器端不会产生跨域错误。生产环境就把前端打包成静态文件丢给Nginx再配置一个location /api反向代理到后端服务思路上是一样的。5.3 健康趋势图与数据可视化这个系统最有展示效果的页面就是统计报表。我用ECharts画血压趋势折线图家属端也能看到老人的健康数据变化。这是整个系统里体验感最强的部分答辩时一定要演示。先封装一个图表组件template div refchartRef :style{ height: height px }/div /template script setup import * as echarts from echarts import { ref, onMounted, onBeforeUnmount } from vue const props defineProps({ height: { type: Number, default: 300 } }) const chartRef ref(null) let chartInstance null onMounted(() { chartInstance echarts.init(chartRef.value) }) onBeforeUnmount(() { chartInstance chartInstance.dispose() }) function setOption(option) { chartInstance chartInstance.setOption(option) } defineExpose({ setOption }) /script在统计页面调用接口获取近30天的血压数据转换成ECharts需要的格式const trendData async (oldPersonId) { const res await request.get(/health-record/trend, { params: { oldPersonId, days: 30, type: 1 } }) chartRef.value.setOption({ xAxis: { type: category, data: res.data.dates }, yAxis: { type: value, name: mmHg }, series: [{ name: 收缩压, type: line, data: res.data.highValues, smooth: true, areaStyle: {} }, { name: 舒张压, type: line, data: res.data.lowValues, smooth: true, areaStyle: {} }] }) }图表加载有个常见问题有时候明明数据接口正常图却显示不出来。十有八九是echarts容器初始化时宽度或高度为0。解决办法是在容器渲染完成后再init如果容器一开始是display:none或者v-if控制了显隐就需要在显隐切换后重新init。我在项目里用nextTick处理const showPanel ref(false) watch(showPanel, (val) { if (val) { nextTick(() { chartInstance echarts.init(chartRef.value) }) } })5.4 页面组件与权限细节老人列表页用Element Plus的Table和Pagination组合el-table :datalist v-loadingloading stripe el-table-column typeindex label序号 width60 / el-table-column propname label姓名 / el-table-column propgender label性别 :formattergenderFormatter / el-table-column propphone label联系电话 / el-table-column propchronicDisease label慢性病史 show-overflow-tooltip / el-table-column label操作 width240 template #default{ row } el-button typeprimary link clickgoDetail(row)详情/el-button el-button typewarning link clickgoEdit(row)编辑/el-button el-button typedanger link clickhandleDelete(row)删除/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequery.page :page-sizequery.size :totaltotal changeloadData /权限控制这里我遇到过一个教学相长的细节前端不能只靠隐藏按钮来实现权限一定要在后端接口再校验一次。比如家属角色调用删除老人接口后端必须判断当前用户角色不是管理员就拒绝。我一般用一个自定义注解在Controller上标注需要的角色比如RequireRole(role 1)再用AOP或者拦截器统一校验这样每个接口只需要加一行注解权限逻辑不会散落到处都是。6. 前后端联调与打包部署6.1 本地联调流程开发阶段前后端是分开跑的。前端npm run dev跑在3000端口后端SpringBoot跑在8080端口。联调的第一步检查后端的接口文档——我一般用Knife4j它是Swagger的增强版页面清爽不用登录就能看接口。前端同学对着接口文档调接口比自己猜路径效率高十倍。联调时我的工作是先按接口文档把Postman的接口测试跑一遍确认后端都通了再让前端对接否则定位问题会分不清是前端的问题还是后端的问题。前后端联调遇到接口返回格式与约定不一致时尽量改后端因为后端是统一返回改一处全改动。6.2 后端打包与前端部署后端打包mvn clean package -DskipTests打包成jar包后直接用命令启动java -jar health-server.jar --spring.profiles.activeprod我习惯在application-prod.yml里配置生产环境的数据源和端口开发和生产配置通过spring.profiles.active切换这样不会出现本地开发配置误连生产库的问题。前端打包npm run build打包产物在dist目录。有两种部署方式第一种是前后端完全分离部署dist文件放到Nginx的html目录Nginx配置代理转发API请求到后端jar包server { listen 80; server_name yourdomain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }第二种是把前端dist目录复制到SpringBoot项目的src/main/resources/static目录下然后重新打包成一个jar。这种方式适合演示场景不需要额外装Nginx。但有个明显的坑Vue Router如果用了history模式刷新页面会404因为SpringBoot不认识前端的路由路径。解决方法是加一个控制器把所有非API路径转发到index.htmlController public class SpaForwardController { RequestMapping(value /{path:[^\\.]*}, produces text/html) public String forward() { return forward:/index.html; } }或者把Vue Router的history模式改为hash模式路径上有#号刷新不会出问题。两种方案我推荐开发期用hash模式方便答辩演示用history模式Nginx更专业。6.3 环境差异坑部署到Linux服务器上很多同学会遇到MySQL连接不上或者启动失败的问题。第一排查防火墙有没有放行3306和8080端口第二检查MySQL的bind-address配置默认只监听127.0.0.1外部机器连不上需要改成0.0.0.0第三注意MySQL 8.0的加密规则如果客户端连的时候报Authentication plugin caching_sha2_password cannot be loaded说明驱动版本太旧用最新版MySQL Connector/J即可。7. 常见问题排查速查表做了这么多前后端分离项目把最容易踩的坑整理成一张排查表遇到问题对号入座就行。现象可能原因解决办法SpringBoot启动报数据源错误MySQL版本和驱动不匹配MySQL 8.0用com.mysql.cj.jdbc.Driver5.x用com.mysql.jdbc.Driver连接报SSL错误连接串没加useSSLfalse加?useSSLfalseserverTimezoneAsia/Shanghai前端请求接口返回401token过期或没带token检查请求拦截器是否设置Authorization头检查token过期时间前端请求接口跨域报错3000端口请求8080端口用Vite代理或后端允许CORS生产环境用Nginx转发数据库存中文变问号数据库或表字符集不是utf8mb4统一改成utf8mb4连接串加characterEncodingutf8Vue页面刷新后404history模式路由刷新时无法匹配Nginx配置try_files或改成hash模式日期显示NaN或时间差8小时前后端时区不一致后端统一用serverTimezoneAsia/Shanghai前端格式化YYYY-MM-DD删除有绑定的老人报外键错误关联表有数据引用设计时不要物理外键删除保护要么软删除要么级联删除Maven依赖下载慢镜像源问题配置阿里云Maven镜像仓库打包后静态资源404前后端合并部署的Spa问题增加forward控制器或者用Nginx部署其中有两个值得单独展开数据库时间字段和前端显示的8小时偏差问题。后端MySQL的datetime是时区无关的如果JDBC连接串没配置时区默认取系统时区而前端浏览器用的又是本地时区两边对不上就会差8小时。统一在连接串和Jackson配置里都用Asia/Shanghai问题迎刃而解。删除老人提示外键约束失败。如果表之间用了数据库物理外键删除主表数据时关联表有记录就会报错。我的实践是数据库层不用物理外键只在代码层维护关系逻辑。这样删除、分页查询的性能更好也不会被外键约束卡住。需要在业务层自己保证数据一致性配合事务处理。8. 写在最后从项目到能力沉淀做这个毕设我最大的体会是系统设计永远比写代码重要。把需求理清楚、把数据库表设计对、把权限边界划分好剩下的代码就是体力活。反过来如果一上来就埋头写代码写着写着发现表结构缺字段、角色权限混乱、接口返回格式五花八门返工成本远远超出预期。对这个项目我个人推荐一条扩展路径接入物联网设备。老人佩戴的手环定时上报心率、步数到系统后端提供一个接收设备数据的HTTP接口前端展示实时数据。这个扩展方案技术上不复杂但会让毕设的档次明显上升——从信息管理系统变成智慧健康监测平台论文的落脚点也更有时代感。还有一条更省事的扩展是消息通知渠道。站内消息第一版已经有后续可以接入短信或者微信公众号模板消息。这样老人血压异常时可以自动发短信给家属而不是等家属自己登录系统看。这个功能在答辩时讲出来评委会觉得你考虑到了真实业务场景。最后分享一个小事我记得项目联调阶段有次家属端图表怎么都不显示排查了半天发现是老年人生日字段传成了字符串导致前端的年龄计算函数返回了NaN进而图表x轴数据全是空值。从那以后我所有接口返回的日期字段都统一格式化成yyyy-MM-dd不在前端做二次解析。这类小坑非常不显眼但每一个都能让人折腾一下午。这个系统做完你收获的不只是一份毕业设计而是一条完整的前后端分离开发流程——从需求分析、数据库建模、接口设计、联调到部署每一步你都亲手走过。面对说说这个系统的架构、为什么选这个技术栈这类问题你心里是有底气的因为每一步都是你自己选择和验证过的。