SpringBoot智慧养老管理系统开发:从需求到答辩全流程解析
写这篇攻略之前我先说说这题怎么来的。养老、健康管理、后台系统这三个词凑在一起就是近三年毕业设计里最稳的选题方向之一。SpringBoot做后端几乎是行业标配前端随便套个Vue或者Thymeleaf就能跑起来工作量适中、技术点覆盖全面、演示效果好答辩时也好讲故事。我前前后后帮人改过好几个类似的系统自己也完整搭过一套这里把从需求分析到答辩准备的整个链路捋一遍尤其是我踩过的那些坑、优化过的那些细节一次性讲清楚。1. 项目整体设计与功能拆解1.1 这套系统到底要解决什么问题智慧养老中心管理系统名字听着大实际上核心就一件事把养老机构里原本靠Excel、微信群、口头交接来管理的信息收拢到一个Web系统里统一处理。老人档案、亲属联系方式、护工排班、床位状态、健康测量记录、用药提醒、费用缴纳这些数据如果散落在不同人手里就会出现经典的“家属问情况前台翻半天找不到记录”的尴尬场景。系统要做的就是把数据集中化、流程线上化、状态可视化。毕设层面的定位要清楚这不是一个商用级SaaS产品而是一个能体现你掌握了Java后端开发主要知识点的完整项目。所以模块不能太少否则撑不起“设计”两个字但也不能过度设计否则两个月做不完。合理的边界是覆盖至少四种角色、五个以上核心业务模块、有一套相对完整的权限控制和数据统计这就足够了。我见过很多同学把精力浪费在花里胡哨的页面动效、或者非要搞微服务拆分上头结果核心的业务逻辑反而写得稀烂。记住答辩老师看的是你的设计思路和代码质量不是看你的页面好不好看。1.2 角色划分与权限边界这套系统我按照实际养老院的组织架构划分了四种角色角色核心职责主要操作权限系统管理员全局配置、账号管理、数据总览全部模块含角色分配、数据备份养老院管理员院长/主管日常运营管理、入住审批、费用审核除系统配置外的所有业务模块护工执行日常护理任务老人档案查看、护理记录填写、健康测量录入、任务执行家属远程关注老人状态仅查看自家老人的档案、健康记录、缴费记录为什么把家属单独拎出来做一个角色因为这是“智慧”二字的重要体现家属通过系统就能看到老人的身体状况和护理动态不用天天打电话问护工。这个设计在答辩时非常好讲它能直接引出“你如何设计权限模型”“如何保证数据隔离”“如何防止越权访问”这一串问题都是我下面要详细说的重点。1.3 功能模块的边界划分我最终落地的功能模块清单是这样的老人档案管理基本信息、入住信息、紧急联系人、健康状态标签护工管理护工资料、排班日历、工作量统计床位管理床位状态实时可视化空闲/已入住/维修中、入住分配与换床健康监测管理血压、血糖、心率、体温等测量数据录入与趋势图护理任务管理任务的创建、指派、执行、确认支持周期性任务模板用药提醒管理药物清单、服药计划、执行记录过期未服药自动标红费用管理床位费、护理费、餐饮费按月生成账单记录缴费状态告警管理健康指标异常、未按时服药、护工漏执行任务自动生成告警记录数据统计看板入住率、健康异常数量、任务完成率等核心指标可视化说句实话光靠我一个人写这套系统大概需要三到四周的业余时间但如果你是毕设集中做每天投入四小时以上三周左右能出完整版本。2. 核心技术选型与技术栈说明2.1 为什么选SpringBoot这套组合SpringBoot现在就是Java后端的事实标准几乎所有Java岗位招聘都要求会它选它做毕设本身就是给简历加分的。它的自动配置让起步成本极低你不用像SSH时代那样配一大堆XML文件一个注解加一个配置文件就能把项目跑起来这让你能把精力放在业务逻辑而不是框架配置上。我选的具体组合是后端框架SpringBoot 2.7.x别选3.x部分老教程和依赖可能不兼容毕设求稳持久层框架MyBatis-Plus数据库MySQL 8.x缓存Redis不做主存储只做验证码、Session共享、热点数据缓存安全框架Spring Security JWT接口文档SpringDoc OpenAPISwagger的替代版前端Vue 2 Element UI管理后台标配组件齐全构建工具Maven有同学会问为什么不用MyBatis而是MyBatis-Plus答案很简单MyBatis-Plus自带分页插件、条件构造器、代码生成器单表CRUD基本不用写SQL这能极大压缩你的工作量。尤其是毕设这种以业务功能为主的项目把时间花在写selectById这种重复代码上纯属浪费。2.2 环境搭建的完整清单这个部分我给的是一份可以直接照着执行的清单避免你卡在环境上磨蹭好几天JDK安装JDK 8如果是SpringBoot 2.x版本JDK 8完全够用且兼容性最好Maven3.6.3以上配置阿里云镜像仓库不然拉依赖能等到你怀疑人生MySQL8.0以上建议本地装不要用云数据库答辩现场断网还能演示RedisWindows用户装Redis时注意官网没有原生Windows版要用微软维护的发行版或WSL方式后面我详细说开发工具IntelliJ IDEA社区版就够别为了破解旗舰版浪费时间前端工具Node.js 14npm或yarn都行环境装完之后我推荐你先跑通一个最小闭环创建SpringBoot项目写一个HelloController浏览器访问到返回结果。这个闭环跑通了后面所有工作都是在这个基础上的增量开发。2.3 目录结构与分层设计的讲究目录结构我直接贴出来这是我几次改版后觉得最清晰的一版com.yanglao ├── common // 全局配置、统一返回结果、异常处理、工具类 │ ├── config │ ├── exception │ ├── result │ └── utils ├── controller // 接口层只做参数接收和结果返回 ├── service // 业务逻辑层核心业务都在这 ├── mapper // 数据访问层继承BaseMapper ├── entity // 数据库实体类 ├── dto // 数据传输对象 ├── vo // 视图对象 ├── security // Spring Security JWT的配置与过滤器 └── YanglaoApplication.java分层设计不是装样子它解决的是实际的维护问题。我见过有人把SQL写死在Controller里后续每次改需求都要动接口层改一处崩三处。分层之后Controller只负责把前端传来的参数交给ServiceService算完业务返回结果Mapper负责跟数据库打交道每一层只做自己的事。答辩时如果你能把这个设计讲清楚老师的第一印象分就拿到了。3. 数据库设计与核心表结构3.1 表关系设计的核心思路数据库设计是答辩必然问到的重点也是整个系统能跑起来的根基。我一共设计了十几张表核心的几张关系是这样组织的老人表elder是业务核心所有业务表都围绕它展开老人与护工是多对多关系通过护理任务表care_task关联老人与床位是一对一关系通过elder表里的bed_id字段关联健康记录health_record与老人是多对一每次测量产生一条记录用药计划medication_plan与用药执行记录medication_log是一对多费用账单fee_bill与老人是多对一按月生成给一个关键的建议关联关系不要过度建外键。逻辑上关联就行物理上不要加外键约束不然删除老人档案时会被一堆外键错误卡住亲测难受。用MyBatis-Plus提供的逻辑删除功能删除时打标记而不是物理删除这样数据始终可追溯。3.2 核心表的字段设计详解拿三张最重要的表展开说其他表照这个思路设计即可。老人表 elderCREATE TABLE elder ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, name varchar(50) NOT NULL COMMENT 姓名, gender tinyint(1) DEFAULT 0 COMMENT 性别 0未知 1男 2女, age int(11) DEFAULT NULL COMMENT 年龄, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, phone varchar(20) DEFAULT NULL COMMENT 本人电话如有, health_level varchar(20) DEFAULT 自理 COMMENT 健康等级自理/半自理/失能, bed_id bigint(20) DEFAULT NULL COMMENT 床位ID, emergency_contact varchar(50) DEFAULT NULL COMMENT 紧急联系人, emergency_phone varchar(20) NOT NULL COMMENT 紧急联系电话, status tinyint(1) DEFAULT 1 COMMENT 状态 0已退住 1在住, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, deleted tinyint(1) DEFAULT 0 COMMENT 逻辑删除 0未删 1已删, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT老人信息表;健康记录表 health_recordCREATE TABLE health_record ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, elder_id bigint(20) NOT NULL COMMENT 老人ID, record_type varchar(20) NOT NULL COMMENT 测量类型血压/血糖/心率/体温, measure_value varchar(50) NOT NULL COMMENT 测量值, measure_unit varchar(20) DEFAULT NULL COMMENT 单位, is_abnormal tinyint(1) DEFAULT 0 COMMENT 是否异常 0正常 1异常, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 测量时间, PRIMARY KEY (id), KEY idx_elder_id (elder_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT健康测量记录表;护理任务表 care_taskCREATE TABLE care_task ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, elder_id bigint(20) NOT NULL COMMENT 老人ID, nurse_id bigint(20) NOT NULL COMMENT 护工ID, task_type varchar(50) NOT NULL COMMENT 任务类型翻身/喂食/清洁/康复训练等, task_content varchar(500) NOT NULL COMMENT 任务内容描述, plan_time datetime NOT NULL COMMENT 计划执行时间, actual_time datetime DEFAULT NULL COMMENT 实际执行时间, status tinyint(1) DEFAULT 0 COMMENT 状态 0待执行 1已完成 2已超时 3已取消, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_nurse_id (nurse_id), KEY idx_elder_id (elder_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT护理任务表;字段设计有个经验所有时间字段统一用datetime不要用timestamp差八小时的问题在定时任务和图表统计时特别坑。所有字符集统一utf8mb4谁知道老人名字里会不会有个生僻字用utf8可能直接存不进去亲测踩过这个坑。3.3 索引设计与查询优化这是很多人忽视的点。表建好了谁都会写但索引设计直接决定了查询性能。核心原则是给查询条件里高频出现的字段建索引给联合查询的关联字段建单列索引。我上面已经在health_record和care_task里加了idx_elder_id和idx_nurse_id这两张表是系统里数据增长最快的两张表没有索引的话数据量上到几万条分页查询就会明显变慢。有一个必加的索引组合care_task表的(status, plan_time)联合索引。因为系统每天都会跑一个定时任务找出所有“待执行但已超过计划时间”的任务标记为超时。如果没有这个联合索引定时任务会全表扫描养老院里几百张任务还好数据积累一段时间后就会拖慢整个系统。4. 核心功能模块的落地实现4.1 登录认证与权限控制的实现要点登录认证我选了Spring Security JWT的方案。为什么不选简单的Session登录因为后续要做前端分离Session跨域麻烦JWT天然适合前后端分离架构。而且JWT在答辩时能引出“无状态认证”“Token过期策略”“刷新机制”这些话题都是加分项。核心思路是用户登录成功后后端生成一个JWT Token返回给前端。前端每次请求都把这个Token放在请求头里一般是Authorization: Bearer xxx后端通过过滤器解析Token识别出当前用户是谁、有什么角色权限。权限控制我做了两级URL级别的角色控制用Spring Security的注解就行PreAuthorize(hasRole(ADMIN)) GetMapping(/admin/dashboard) public Result getDashboardData() { // 只有管理员可以访问 }以及数据级别的隔离控制。家属登录后不能看其他老人的数据这个不能用注解解决得在Service层写死Service public class HealthRecordServiceImpl extends ServiceImplHealthRecordMapper, HealthRecord implements HealthRecordService { Override public PageHealthRecord listRecordsByElder(Long elderId, Long currentUserId, String currentUserRole) { // 家属角色只能查自己的老人 if (FAMILY.equals(currentUserRole)) { Elder elder elderService.getById(elderId); if (elder null || !elder.getEmergencyPhone().equals(currentUserPhone)) { throw new BusinessException(无权查看该老人的健康记录); } } // 正常分页查询 LambdaQueryWrapperHealthRecord wrapper new LambdaQueryWrapper(); wrapper.eq(HealthRecord::getElderId, elderId) .orderByDesc(HealthRecord::getCreateTime); return this.page(new Page(pageNum, pageSize), wrapper); } }这里有个细节我故意在报错时不提示“该老人不存在”只提示“无权查看”。原因很简单避免恶意用户通过接口尝试遍历老人ID根据返回结果的差异判断某个老人是否存在。安全设计要在细节里体现。4.2 老人档案与床位管理的实现细节老人档案管理本质是增删改查但有两个值得展开的点入住流程和换床流程。入住流程我设计为选择空闲床位 - 填写老人信息和紧急联系人 - 绑定床位 - 生成入住记录。这里核心是床位状态的原子性更新避免两个管理员同时给两个老人分配同一个床位。用MyBatis-Plus的乐观锁插件解决给bed表加一个version字段更新时set bed_status1 where id? and version?更新失败则提示“床位已被占用请刷新重试”。换床流程更隐蔽。老人从A床换到B床要同时更新A床为空闲、B床为占用、老人表的bed_id改为B床。这三个操作必须放在同一个事务里否则会出现老人显示在B床但B床还是空闲状态的脏数据。这就是Spring事务注解的典型用法Transactional(rollbackFor Exception.class) public void changeBed(Long elderId, Long oldBedId, Long newBedId) { // 1.验证新床位状态 // 2.更新老人bed_id // 3.旧床置空 // 4.新床置占用 }前端展示床位状态时我建议用一份截面数据接口把床位表查出来分组为空闲、已入住、维修中三种状态前端用不同颜色渲染。演示效果很好视觉冲击力强老师一看就知道这系统是用了心的。4.3 健康数据录入与趋势图分析这是整个系统里最出彩的一个模块也是答辩时的核心亮点。健康数据录入的交互有两种手动录入和批量导入。手动录入就是选老人、选类型、填数值、提交。批量导入跟Excel打交道前端上传Excel文件后端用EasyExcel解析按下划线拆字段塞进数据库。批量导入这种点我强烈建议做一个演示时快速导入几十条数据生成图表效果非常震撼。健康数据最有技术含量的是趋势图分析。前端用ECharts画折线图后端提供按老人、按时间范围、按指标类型查询的数据接口GetMapping(/statistics/{elderId}) public Result getHealthStatistics(PathVariable Long elderId, RequestParam String type, // blood_pressure / blood_sugar / heart_rate / temperature RequestParam String startDate, RequestParam String endDate) { // 按日期分组聚合返回 { date: 2024-01-15, value: 121 } ListMapString, Object dataList healthRecordService.getTrendData(elderId, type, startDate, endDate); return Result.success(dataList); }异常判断规则我是这样设计的心率正常范围60-100次/分体温36.0-37.3度血压收缩压90-140mmHg且舒张压60-90mmHg血糖空腹3.9-6.1mmol/L。录入时后端实时校验超出范围自动标记为异常并生成告警记录。系统上线后可以跟养老院的医生团队确认实际参考范围不同人群特别是老年慢病患者的正常区间跟教科书上的不完全一致但毕设阶段按标准范围写就够用。这里还要注意ECharts的X轴时间格式化问题。如果你直接传2024-01-15 08:30:00这种完整时间戳图上会密密麻麻全是点。正确做法是后端按天聚合返回精简后的日期字符串前端配置xAxis.type category和axisLabel.formatter参数图表的可读性和美观度才够。4.4 自动告警的定时任务实现自动告警是系统“智慧”二字的直接体现实现方案我建议用SpringBoot自带的Scheduled注解简单可靠不需要额外引入XXL-Job这种重武器。我设置了两个定时任务第一个每5分钟跑一次扫描健康记录表找出最新的测量值有异常的记录生成告警消息发送给相关护工。第二个每小时跑一次扫描护理任务表找出所有“待执行”但计划时间已经过期的任务状态改为“已超时”同时生成一条超时未处理告警。定时任务的代码本身不复杂但有几个坑必须提前说第一Scheduled默认是单线程串行执行的。如果你定义了多个定时任务它们的执行时间可能互相影响甚至出现任务堆积后错乱。解决方法是加一个配置类Configuration public class ScheduleConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } }第二定时任务跑的时候要考虑数据库压力。健康记录如果是每30分钟录入一次全表扫描判断“最新的一条是否异常”是低效的。我的做法是先按elder_id分组查最新测量时间再只查这些时间对应的记录。这比全表扫描快一个数量级。代码写出来也不复杂// 只查询每个老人最近一条健康记录判断是否有异常 ListElder allElders elderService.list(); for (Elder elder : allElders) { HealthRecord latest healthRecordService.getLatestRecord(elder.getId()); if (latest ! null latest.getIsAbnormal() 1) { // 生成告警记录 } }第三定时任务要支持“幂等”。同一个任务不能重复执行造成重复告警数据库里加一个判断字段处理过的记录更新状态标记。4.5 数据统计看板的设计与SQL实现数据看板是整个系统的“脸面”也是答辩时的视觉核心。我做的看板包含这几个核心指标在住老人总数今日新增入住人数今日健康异常告警数量今日任务完成率各健康等级人数分布饼图近7日健康测量趋势折线图床位使用率进度条这些指标背后对应着一堆聚合SQL。以“任务完成率”为例我写的是SELECT COUNT(*) AS total_task, SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) AS completed_task, ROUND(SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS complete_rate FROM care_task WHERE DATE(plan_time) CURDATE();SQL能跑通但更推荐把聚合逻辑放到Mapper层用XML或注解写不要把SQL拼在Service层。我当时是把看板需要的五六个聚合查询统一封装在一个DashboardMapper接口里一个方法一条SQL前端一个接口一次性获取全部指标数据。不仅代码清晰还避免了一次前端展示调五六次后端接口的尴尬。Redis在这里派上了用场。看板数据不需要每次刷新都实时查库除了在住老人总数这种变化极低频的数据其余指标我做了60秒的本地缓存或Redis缓存性能有明显提升。演示时这还能作为优化点讲。5. 前端功能实现与前后端联调5.1 前端路由与菜单权限的动态生成如果前端用了Vue Element UI菜单权限这块大概率是你遇到的第一个复杂问题。做法不是在前端写死菜单而是登录后根据当前用户的角色动态生成菜单数组路由用addRoutes动态注册这样不同角色进入系统看到的菜单完全不同。比如护工登录后只看到“护理任务”“健康录入”“我的老人”这几个菜单项家属只会看到一个“我的家人”的详情页管理员看全部。这在答辩时是很容易被问到的点因为权限控制如果只做了后端接口拦截前端菜单不可控界面上按钮还杵在那点一下才提示无权访问体验很差。后端接口返回的菜单结构大概长这样{ code: 200, data: { menus: [ { path: /dashboard, name: 工作台, icon: el-icon-odometer }, { path: /health, name: 健康管理, children: [ { path: /health/record, name: 测量记录 }, { path: /health/statistics, name: 趋势分析 } ]} ], userInfo: { name: 王护工, role: NURSE } } }前端拿到这个结构遍历渲染sidebar菜单路由守卫里做动态路由注册。有一个坑需要注意addRoutes之后刷新页面路由会丢失。解决方法是把用户信息和权限菜单缓存到localStorage或Vuex并持久化在beforeEach钩子里重新从store取出并通过addRoutes重新注册路由。5.2 通用封装请求库、组件与样式统一前后端联调阶段前端开发效率的决定因素是封装的完整度。与其每个页面单独写axios.get然后手动处理返回结构不如一开始就封装一个统一的请求工具import axios from axios; import { Message } from element-ui; import store from /store; import router from /router; const service axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器自动携带 Token service.interceptors.request.use(config { const token store.getters.token; if (token) { config.headers[Authorization] Bearer ${token}; } return config; }, error Promise.reject(error)); // 响应拦截器统一处理错误信息 service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Message.error(res.message || 请求失败); if (res.code 401) { store.dispatch(logout).then(() router.push(/login)); } return Promise.reject(new Error(res.message)); } return res; }, error { Message.error(error.response?.data?.message || 网络异常); return Promise.reject(error); } ); export default service;注意后端接口统一返回格式很关键。我定义的世界观是这样的{ code: 200, message: success, data: {} }code不为200时前端直接弹错误提示。401表示Token过期或未登录拦截器里直接踢回登录页。这套封装在后端接口约定统一后前端写页面代码时只需要关注业务数据不需要每次写错误处理逻辑开发效率提升非常明显。组件层面我把老人选择器、日期范围选择器、分页表格都抽成了公共组件几顿饭的功夫省下后面几天的工作量。样式层面没有用各种花哨的UI框架就Element UI默认主题加上一套统一的间距和色彩规范看起来清清爽爽。记住毕设页面不需要惊艳需要的是整洁与统一。5.3 核心页面的交互细节与演示效果几个核心页面的交互设计我在做的时候有一些细节上的考虑老人列表页搜索区放在顶部姓名、身份证、健康等级、床位号、表格列清晰、操作按钮详情/编辑/换床/退住用图标按钮减少视觉干扰、分页放在底部。表格行的重点字段比如健康等级是“失能”用红色标签突出显示。床位管理页以楼栋、楼层两个维度组织每层是一个横向卡片里面一排床位格子。已入住是绿色空闲是白色维修中是黄色。点击空闲床位弹出入住老人表单点击已入住床位弹出老人卡片和“换床/退住”操作。整个页面不需要表格全用卡片和颜色表达状态演示时视觉反馈极好。健康趋势页顶部四个指标Tab血压/血糖/心率/体温每个Tab一个折线图。支持按时间范围筛选鼠标悬浮可以看到每天的测量值和是否异常。异常点用红色圆点标记一眼扫过去就能看到问题在哪里。告警中心页表格列出所有未处理的告警记录每条含老人姓名、告警类型、告警时间、处理状态。点击“处理”按钮可以填写处理结果。加一个筛选条件“只看今日”演示时能快速展示系统实时监测的效果。这些细节不复杂但汇总起来的效果就是老师从登录开始每个页面都有亮点可以讲解不会出现“这个页面就是个普通表格”的尴尬评价。6. 常见问题与排查技巧实录6.1 数据库连接与字符集问题我几乎每次帮人看毕设项目遇到的第一个问题都出在数据库连接上。最常见的报错是Access denied for user rootlocalhost排查思路很简单确认MySQL服务有没有启动Windows服务管理器里看看确认密码对不对注意MySQL 8.x默认安装了密码加密插件caching_sha2_password如果你的JDBC驱动版本太老会连不上换最新版MySQL Connector/J即可。字符集问题是另一个高发坑。统一用utf8mb4之后还要注意数据库连接串上加参数jdbc:mysql://localhost:3306/yanglao?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiserverTimezoneAsia/Shanghai这个参数不加时间字段查询出来会差8个小时很多人找半天都不知道是这里的问题。加了之后数据库时间、Java时间、前端显示时间才完全统一。6.2 MyBatis-Plus的使用细节与常见报错MyBatis-Plus虽然好用但有几个细节不处理好很容易被卡住。第一个是分页插件不生效。很多人配了分页但是selectPage返回的总数永远是0原因是你没有配置MyBatis-Plus的分页插件。需要在配置类里注册Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }第二个是实体类字段映射问题。数据库字段是create_timeJava实体是createTime如果开启了驼峰映射会自动对应但有些复杂查询的resultMap没配好就会查出来一堆null。检查一下application.yml里的配置mybatis-plus: configuration: map-underscore-to-camel-case: true第三个是逻辑删除的配置。我在表设计里都加了deleted字段那么实体类对应的属性上要加注解同时全局配置里声明逻辑值TableLogic private Integer deleted;mybatis-plus: global-config: db-config: logic-delete-value: 1 logic-not-delete-value: 0不配置的话删除操作是物理删除数据不可恢复答辩时被老师问一句“表结构里为什么有deleted字段但删除却是物理的”就会很尴尬。6.3 JWT登录失效与跨域调试问题JWT这块我见过的最经典错误是登录成功了但后续所有需要鉴权的接口都返回401。排查步骤是先确认前端请求头是否带了Token用浏览器的开发者工具Network面板看一下请求头。再看后端的JWT过滤器有没有正确注册到过滤器链里Spring Security的配置顺序搞错的话过滤器压根不会执行。另一个问题是跨域。前端8080端口访问后端8081端口会有跨域问题后端是SpringBoot项目配置了端口8081前端是Vue的开发服务器默认8080。但如果你用了统一请求路径/api是通过前端Vite/Webpack的proxy代理到后端的就不存在跨域问题。我建议用代理方式因为代码里不需要加任何跨域配置生产部署也方便。代理配置在Vue项目的vue.config.js里module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } };如果你确实需要在后端加跨域配置用CorsFilter即可但这个方法写起来稍微繁琐还容易配错不如前端代理省心。两种方案面试时都能讲但项目里建议用代理。6.4 部署打包的完整流程答辩前一定要把系统打包成能独立运行的jar包或者部署脚本别到时候现场起不来项目。我的经验是这样的数据库脚本导出成init.sql答辩前在老师电脑或自己电脑上先跑一遍确保脚本完整可执行。很多人平时开发是手动建表建数据临时导出脚本的时候漏掉了数据现场演示时表格是空的效果大打折扣。后端打包命令非常简单mvn clean package -Dmaven.test.skiptrue生成的jar包在target目录下运行java -jar yanglao-system.jar --spring.profiles.activeprod建议把application.yml里数据库密码改成环境变量或者外部配置文件的方式避免打包时把本地密码带进jar包里。答辩现场换机器部署时改外部配置即可不用重新打包。前端打包npm run build生成dist目录里面是静态文件。最简单的部署方式是把dist目录扔到后端的src/main/resources/static目录里重新打包后端一个jar包全搞定不需要Nginx。虽然不建议生产环境这么干但答辩场景图省事、图稳定绝对是最优解。我用这种方式部署后换电脑演示时只要装了JDK和MySQL一条命令就能跑起来老师基本不会遇到环境问题。6.5 性能优化与答辩加分点如果时间充裕有几个优化点做完能在答辩时明显加分第一健康记录的查询优化。数据量起来后按老人和时间范围查询需要索引支撑。我经常给health_record表建议加一个联合索引(elder_id, create_time DESC)一次性解决90%以上的查询场景。第二缓存优化。老人档案这种变更频率极低的数据用Redis缓存后查询性能能提升一个数量级。关键是缓存失效时机的设计写操作后主动清缓存保证数据一致性。第三接口响应时间优化。给核心接口加上简单的耗时统计日志答辩时展示“健康趋势查询接口平均响应时间XXms命中缓存后降至XXms”这类数据非常有说服力。实现也很简单一个Aspect切面就能搞定Aspect Component public class TimeAspect { Around(execution(* com.yanglao.controller..*.*(..))) public Object logTime(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); long cost System.currentTimeMillis() - start; System.out.println(pjp.getSignature().getName() 耗时: cost ms); return result; } }这些优化点不需要全做完挑一两个做扎实就足够在答辩时体现“我做过性能调优”的深度。7. 答辩准备与避坑建议7.1 答辩前一定要准备的几个问题答辩本质上是对项目真实性和技术深度的检验。老师大概率会问这几类问题提前准备就好第一类是需求设计问题“你为什么要做这个系统”“系统解决了什么实际问题”。回答要落到具体的痛点场景上比如“养老院以前用纸质表格记录健康数据家属问情况只能打电话效率低且信息不透明”别空谈概念。第二类是技术选型问题“为什么选SpringBoot不选其他框架”“MyBatis-Plus和MyBatis有什么区别”。重点是展示你做过比较和思考“SpringBoot简化了配置和部署配合Starter生态能快速集成安全框架、缓存、模板引擎非常适合这类中小型管理系统”。第三类是安全性问题“如果用户Token泄露了怎么办”“你的密码是明文存储的吗”。密码必须加密存储用BCryptPasswordEncoder这是最基本的底线。Token过期策略也能讲一讲我这里用的是JWT默认不主动失效过期时间2小时配合Redis做黑名单可以实现真正的强制下线但毕设里用简单的过期策略完全够。第四类是扩展性问题“如果以后要接入智能硬件设备比如智能手环你的系统怎么改”。这个问题准备一个简单的思路即可在健康记录表增加一个数据来源字段预留一个设备数据接收接口通过消息队列异步写入不阻塞现有业务。7.2 代码规范与文档写作的准备工作答辩最终是由人来看的代码整洁度会直接影响老师的判断。至少做到几条硬性规范类名、方法名、变量名有明确含义不写拼音缩写更不要用无意义的a1、b2方法加上注释抽象出公共逻辑不出现大段复制粘贴的重复代码异常不被吞掉catch以后至少要打日志。文档方面需求分析文档要画清楚用例图可以用工具画好后截图放进Word、数据库设计文档要附ER图加核心表字段说明。这些材料提前准备答辩时摆在老师面前比现场口述有说服力得多。系统演示的流程脚本也要提前练熟。从登录、看板、新增老人、分配床位、录入健康数据、查看趋势图、触发告警、处理告警的完整链路要走一遍记清楚每个操作点的数据变化。建议准备一份极简脚本提前在答辩用的电脑上把所有数据准备到位比如演示时需要一个异常数据来展示告警逻辑就别现场等定时任务跑直接准备一条已触发的告警记录就好。7.3 我踩过的坑与最后的真心话最后分享几个我在实际做这类项目时踩过最深的坑。第一个坑是数据库设计阶段图省事把老人的紧急联系人直接存成字符串字段后面做家属登录、权限隔离时发现没法关联用户体系只能重新加表。正确做法是家属单独建一个family_member表跟老人表是多对一关系每个家属记录有独立登录账号跟系统用户表关联。这个教训的价值在于设计阶段多思考一分钟实现阶段能少踩一个一天的坑。第二个坑是前端图表数据格式跟ECharts对不上。后端返回的是{createTime: 2024-01-15 08:30:00, value: 121}ECharts要的是[2024-01-15, 121]这种扁平数组。改来改去浪费时间。正确做法是在接口层就定义清晰VO按前端需要的数据结构返回前端拿到后直接赋值给图表配置不做二次加工。第三个坑是答辩前才想起准备部署演示环境结果发现学校机房电脑没装JDK、MySQL密码不对、数据库导出脚本不完整现场本来就紧张再被环境问题一卡效果直接折扣。后来我的做法是提前一周就借好答辩用的电脑把所有环境装好、跑一遍完整流程数据备份一份放在U盘里。这套方法你们直接抄就行。我猜你大概率是想安安稳稳把这个毕设做出来、答辩顺利通过。那我最后说一句掏心窝的话毕设项目的意义不在于做一个多么宏大的系统而在于完整走一遍从需求分析、数据库设计、后端开发、前端联调、测试部署的过程这个过程中养成的工程习惯、踩坑经验才是比分数更值钱的东西。照着这篇攻略一步步做你至少能少走一半弯路。祝答辩顺利。