每年到这个节点总有一批学生拿着选题列表来问我“学长/老师你看我这个毕设题目行不行”说实话学生管理系统、图书管理系统、电商商城这类题目我见得太多了不是说不好而是答辩时老师一听题目就没有追问的兴趣因为每年都有十几个一模一样的。相比之下“SpringBootVue 大健康养老公寓管理系统”这个题是少数几个我第一眼看到就觉得“能聊出东西来”的题目。这个项目本质上是一个前后端分离的管理平台后端用 Java 技术栈SpringBoot MyBatis/MyBatis-Plus MySQL前端用 Vue配合 Element UI / Element Plus核心场景是养老公寓的日常运营管理重点突出“大健康”概念——也就是在传统公寓管理床位、费用、入住退住之上增加了老人健康档案、体检记录、健康指标趋势分析、护理任务、用药提醒这些医疗健康相关的模块。它适合三类人一是正在找毕设/课设题目的计算机相关专业学生二是想系统学习前后端分离项目写法的初学者三是想快速搭一个完整项目做二次开发或作品集的人。我帮不少学生看过类似的选题也实际带过一个模拟项目X的完整开发过程。这篇文章就把这个项目的整体设计、数据库核心表、实操落地步骤、常见坑和答辩扩展思路一次性讲透能帮你少走很多弯路。1. 项目定位与功能架构拆解1.1 为什么“养老公寓管理”是个值得做的题目先说说这个选题为什么好。第一业务场景足够完整。一个养老公寓管理系统涉及的业务流程是成体系的老人入住登记 → 分配床位 → 建立健康档案 → 制定护理计划 → 日常护理记录 → 用药提醒 → 费用结算 → 退住办理。这串流程下来系统需要管理的实体包括老人、床位、护理人员、健康数据、费用账单、家属访客等天然就是一个“多模块、有关系、有状态流转”的管理系统。这样的项目放在简历上你可以理直气壮地说“我设计了一个覆盖入住到退住全流程的管理平台”而不是“我写了一个增删改查”。第二健康管理这条线是加分项。普通公寓管理系统做的是“住”而这个系统加上大健康概念后做的是“住 养 护”。你可以做健康指标血压、血糖、心率的录入和趋势可视化可以做护理任务的派发与完成闭环可以做用药提醒。这些功能点既能体现你对业务的理解又能在答辩时展开讲很多细节完全避开“你的系统是不是就是一个 CRUD”这种灵魂拷问。第三业务复杂度适中。它和管理系统最核心的区别是多了一个“健康档案”概念但数据模型并没有复杂到像真正的医疗系统那样需要处理诊断、处方、医保结算。也就是说一个本科生用一套 SpringBoot Vue 的常规技术栈完全可以驾驭不会做到一半无从下手。1.2 角色划分与核心功能模块先明确系统里的角色这决定了权限设计和菜单设计。养老公寓的日常运营中最核心的使用者是管理员和护理员另外还有老人家属这个“外部查询者”角色。实际做毕设时我建议把角色拆成三类系统管理员admin负责全局管理包括老人档案、床位分配、人员管理、费用管理、系统设置、数据统计。这是权限最高的角色。护理员caregiver负责接收护理任务、填写护理记录、录入老人健康指标、执行用药提醒。这个角色的核心场景是“移动端或工作台的日常操作”。家属family只读角色可以查看老人的基本信息、健康趋势、护理记录和费用账单。家属通常没有写权限。在此基础上功能模块可以拆成下面这张表模块核心功能关键实体登录认证账号密码登录、JWT 鉴权、角色路由用户表、角色表老人档案管理新增/编辑/查询老人信息、入住退住、档案导出老人信息表床位管理床位新增、分配、释放、状态看板空床/占用/保洁床位表健康档案管理体检记录、健康指标录入、指标趋势图ECharts健康记录表护理任务管理任务创建、分配、执行、完成状态跟踪护理任务表用药提醒管理用药计划、按时提醒、执行反馈用药提醒表费用管理床位费/护理费/餐费生成、账单查询、缴费标记费用账单表访客管理来访登记、访客记录查询访客记录表系统管理用户管理、角色权限分配、操作日志用户/角色/日志表数据可视化老人入住统计、健康异常告警数量、床位利用率聚合查询对于毕设来说你不需要把所有模块都做成完整的高保真页面但至少要把老人档案、健康档案、护理任务、床位、费用这五个主线串起来形成一个“数据流转闭环”。管理员录入老人信息并分配床位护理员在健康档案里录入血压血糖在护理任务里处理任务费用模块根据入住时间和护理等级自动生成账单家属端可以查看老人健康趋势。这样一来你的项目就有了一条清晰的主线故事。1.3 技术选型解析为什么是 SpringBoot Vue MySQL这个技术栈组合在求职和毕设场景下几乎是“标准答案”级别的存在但它的合理之处值得展开说。后端选 SpringBoot最重要原因是它“约定优于配置”的理念让开发效率极高。你不需要像 SSM 时代那样写一大堆 XML 配置只需要一个启动类加几个注解就能跑起来。同时 SpringBoot 的生态非常成熟集成 MyBatis-Plus、JWT、Redis、Swagger 都有大量文档和现成方案遇到问题基本一搜就有答案。ORM 层我建议用 MyBatis-Plus 而不是原生 MyBatis。理由很直接毕设项目的时间通常只有一到两个月MyBatis-Plus 提供了通用 CRUD 接口、条件构造器LambdaQueryWrapper、分页插件、逻辑删除可以把“增删改查”的代码量减少一半以上。而且它和 MyBatis 完全兼容后期如果想去手写 SQL 优化性能随时可以在 Mapper 里写自定义 SQL不会被框架限制住。MySQL 没什么好说的开源、易得、大家都会用。前端选 Vue 生态具体版本上我建议这样取舍如果你熟悉 Vue 2 且不想踩太多坑选 Vue 2 Element UI Vue Router Axios ECharts这套组合在社区里有海量现成代码稳定成熟。如果你愿意多花点时间选 Vue 3 Vite Element Plus Pinia 也可以但要注意 Element Plus 对 Vue 2 代码有一些不兼容的改名比如el-dialog的visible改成了v-modelsize属性写法也有变化遇到问题要学会查官方文档。权限认证方面我建议用 JWT 拦截器而不是直接上 Spring Security。为什么Spring Security 功能强大但配置链路长、概念多过滤器链、UserDetailsService、AuthenticationManager对初学者来说很容易“配好了但说不清原理”。而 JWT 拦截器的方式足够清晰登录成功后生成 token前端存到 localStorage每次请求带着 token后端用拦截器校验 token 并解析出用户信息。这个方案对于毕设来说完全够用答辩时你还能把无状态认证的原理讲得头头是道。2. 数据库设计与核心技术方案解析2.1 核心表结构设计思路数据库设计是这种管理系统的地基表结构设计得好后面写代码就很顺畅设计得烂后面每个查询都会很痛苦。以下这几张核心表是我反复调整后觉得最合理的方案可以直接参考。先看老人信息表elder字段名类型说明idbigint主键elder_novarchar(20)老人编号如 YZ001namevarchar(50)姓名gendervarchar(10)性别birthdaydate出生日期id_cardvarchar(18)身份证号phonevarchar(20)本人电话可能没有emergency_contactvarchar(50)紧急联系人emergency_phonevarchar(20)紧急联系电话health_statusvarchar(100)当前健康状况摘要care_levelvarchar(20)护理等级自理/半自理/全护理enter_datedate入住日期leave_datedate退住日期空表示在住bed_idbigint关联床位表statusint0退住 1在住remarkvarchar(500)备注这张表要特别注意两个字段care_level和bed_id。care_level直接关系到费用计算和护理任务的生成策略是业务核心bed_id关联床位后就可以在床位管理页面展示“谁住在哪张床”。健康记录表health_record是体现“大健康”特色的核心表字段名类型说明idbigint主键elder_idbigint关联老人record_datedatetime记录时间blood_pressurevarchar(20)血压如 120/80blood_sugardecimal(5,2)血糖 mmol/Lheart_rateint心率 次/分temperaturedecimal(4,1)体温 ℃weightdecimal(5,2)体重 kgoxygenint血氧饱和度 %mental_statevarchar(100)精神状态备注create_byvarchar(50)录入人remarkvarchar(500)备注我刻意把血压设计成varchar而不是拆成收缩压和舒张压两个数字字段。原因是前端采集时用户往往直接输入“120/80”这样的格式拆成两个字段反而让录入变得别扭。如果以后要做血压异常判定可以在后端解析字符串或者在上报接口里拆开存储。毕设这个场景直接存原始字符串是最省事的。用户与权限相关的表按经典的 RBAC 模型来设计sys_user用户表字段包括用户名、密码BCrypt 加密、昵称、状态、角色ID。sys_role角色表字段包括角色编码admin/caregiver/family、角色名称、备注。sys_menu菜单表字段包括菜单名称、父级ID、路由地址、组件路径、权限标识。如果是精简版设计也可以不做 sys_menu而是在前端路由里写死角色对应的菜单后端只返回用户角色。这样做能减少不少工作量但答辩时可能会有老师问“你的动态权限是怎么实现的”所以建议至少把菜单表加上哪怕只存两级菜单。2.2 登录鉴权与权限控制方案登录鉴权这块我详细说下 JWT 的实现路径因为这是很多人容易卡住的地方。整个流程是这样的用户输入用户名密码登录后端用 UserService 查询用户比对密码存的是 BCrypt 加密后的结果比对成功就生成一个 token 返回给前端。这个 token 本质上是一串经过签名加密的 JSON 数据里面包含 userId、username、角色 role 以及过期时间。前端拿到 token 后存起来之后每次请求都在 header 里带Authorization: Bearer token。后端的拦截器负责校验每个请求的 tokenpublic class TokenInterceptor implements HandlerInterceptor { private JwtUtils jwtUtils; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims jwtUtils.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\token无效或已过期\}); return false; } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录\}); return false; } }生成 token 的工具类核心方法大致如下public String generateToken(Long userId, String username, String role) { return Jwts.builder() .claim(userId, userId) .claim(username, username) .claim(role, role) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) // 24小时 .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }这里有一个很关键的点拦截器放行的白名单。登录接口/api/login和静态资源请求必须放行否则用户根本没登录就被拦住了。如果你是直接用WebMvcConfigurer注册拦截器记得指定好 excludePathPatternsregistry.addInterceptor(tokenInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/login, /api/captcha, /error);角色权限这块最简单的做法是在拦截器里解析出role之后对某些接口做角色判断。比如费用管理和系统管理接口只允许 admin 访问。你可以在 Service 层写一个RoleChecker工具类也可以在拦截器里统一处理。对于毕设来说不用做得太复杂但至少要做到“家属登录后只能 GET 查询不能 POST 修改”。2.3 前后端接口规范与统一返回结构前后端分离项目最怕“各写各的”所以在动手开发之前一定要定好接口规范。我常用的统一返回结构是public class ResultT { private Integer code; // 200 成功 400 参数错误 401 未登录 500 服务器异常 private String msg; // 提示信息 private T data; // 返回数据 }对应的 JSON 长这样{ code: 200, msg: 查询成功, data: { id: 1, name: 张奶奶, bedNo: A-203 } }分页接口统一参数page当前页码、limit每页条数、keyword搜索关键字。后端返回一个PageResult包含total、pages、records三个字段。前端用 Element UI 的el-pagination把total绑定到分页组件的total属性上就能非常顺滑地做翻页。时间字段统一格式化在application.yml里配置全局时间格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样后端返回Date类型时前端收到的直接就是格式化好的字符串不用再额外写 formatter。3. 从零到一的实操落地过程3.1 后端工程搭建与核心配置后端工程我建议用 Spring Initializr 来创建地址是 start.spring.io国内也可以直接用 IDE 自带的创建工具。需要勾选的依赖有Spring Web、MySQL Driver、Lombok。然后在pom.xml里手动引入 MyBatis-Plus 和 JWT 相关依赖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 groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.18/version /dependencyhutool 这个工具库强烈建议加上里面提供了身份证校验、日期操作、加密、Excel 导出等常用工具能在写代码时省下大量时间。application.yml的核心配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/elder_care?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有两个配置细节值得注意。第一logic-delete-field: deleted表示表里的deleted字段是逻辑删除标记MyBatis-Plus 会在帮你生成的 delete 方法里自动改成 update 语句查询时自动加where deleted 0这样误删数据还能恢复。第二map-underscore-to-camel-case开启后数据库字段health_status可以自动映射到 Java 类的healthStatus不用每个字段都写TableField。在项目结构上建议按这种分包方式com.example.eldercare ├── common // 统一返回Result、全局异常处理、常量 ├── config // 拦截器、WebMvc配置、MyBatis-Plus分页插件配置 ├── controller // 接口层 ├── entity // 数据库实体类 ├── mapper // MyBatis-Plus Mapper接口 ├── service // 业务逻辑层 ├── utils // JwtUtils、DateUtils等工具类 └── ElderCareApplication.java3.2 前端工程搭建与登录流程前端我用 Vue CLI 脚手架创建项目指定 Vue 2 版本vue create elder-care-web # 进入项目目录后安装依赖 npm install element-ui axios vue-router echartsmain.js里注册全局组件import Vue from vue import App from ./App.vue import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import router from ./router import axios from ./utils/request Vue.use(ElementUI, { size: small }) Vue.prototype.$axios axios new Vue({ router, render: h h(App) }).$mount(#app)axios 封装是前端项目的重中之重我一般会单独建一个utils/request.jsimport axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带token 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) { return res } if (res.code 401) { localStorage.removeItem(token) router.push(/login) } Message.error(res.msg) return Promise.reject(res) }, error { Message.error(error.message) return Promise.reject(error) } ) export default service登录页的逻辑很简单表单校验通过后调用/login接口成功后把 token 存入 localStorage然后跳到首页。路由守卫里加一层判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else if (!token) { next(/login) } else { next() } })3.3 核心模块实现演示老人档案与健康趋势老人档案管理是“最标准的 CRUD”但要注意分页搜索这个细节。Controller 层接收page、limit、keyword三个参数Service 层用 LambdaQueryWrapper 构造条件public PageResultElderVO pageQuery(int page, int limit, String keyword) { PageElder pageParam new Page(page, limit); LambdaQueryWrapperElder wrapper new LambdaQueryWrapper(); if (StrUtil.isNotBlank(keyword)) { wrapper.like(Elder::getName, keyword) .or() .like(Elder::getElderNo, keyword); } wrapper.eq(Elder::getStatus, 1); // 只查在住老人 wrapper.orderByDesc(Elder::getEnterDate); PageElder result elderMapper.selectPage(pageParam, wrapper); // 转换为VO补充床位编号、年龄等信息 return PageResult.of(result.getTotal(), elderVOList); }健康指标趋势图是项目的核心亮点。后端提供一个查询接口返回某个老人最近 7 天的健康记录GetMapping(/elder/{id}/health/trend) public ResultListHealthTrendVO trend(PathVariable Long id) { LambdaQueryWrapperHealthRecord wrapper new LambdaQueryWrapper(); wrapper.eq(HealthRecord::getElderId, id) .orderByDesc(HealthRecord::getRecordDate) .last(limit 7); ListHealthRecord records healthRecordMapper.selectList(wrapper); // 反转列表变成时间从早到晚 Collections.reverse(records); ListHealthTrendVO voList records.stream().map(r - { HealthTrendVO vo new HealthTrendVO(); vo.setRecordDate(r.getRecordDate()); vo.setBloodSugar(r.getBloodSugar()); vo.setHeartRate(r.getHeartRate()); return vo; }).collect(Collectors.toList()); return Result.success(voList); }前端用 ECharts 折线图展示const chart echarts.init(document.getElementById(healthChart)) chart.setOption({ title: { text: 最近7天血糖变化 }, tooltip: { trigger: axis }, xAxis: { type: category, data: dates }, yAxis: { type: value, name: mmol/L }, series: [{ name: 血糖, type: line, data: values, smooth: true, areaStyle: { opacity: 0.15 } }] })这个模块做好了答辩时就可以现场演示“选择一位老人进入健康档案页面可以清楚看到近一周的血糖变化曲线如果数值持续偏高系统会提示护理员重点关注。”这就是整个项目从单纯管理系统升级为“健康管理系统”的最有力证据。3.4 代码联调与基础部署开发完成后的联调阶段我的习惯是先用 Postman 把后端接口全部测一遍状态码和返回结构都对了再开始联调前端页面。这样能避免“页面报错但不知道是后端问题还是前端问题”的尴尬。部署方式上最省事的是前后端分离部署后端打成 jar 包放到服务器上跑前端用npm run build打包成 dist 目录交给 Nginx 托管。Nginx 配置里要把/api开头的请求代理到后端服务server { listen 80; server_name your-domain; location / { root /var/www/elder-care-web; 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; } }如果只是本地演示后端直接mvn spring-boot:run前端npm run serve再用 vite 或 vue.config.js 里配一个 proxy 转发/api就行部署方式不用搞得太复杂。4. 常见问题与排查技巧实录4.1 跨域问题开发环境与生产环境的两套处理前后端分离最容易翻车的坑就是跨域。开发环境下我推荐用前端代理来解决不推荐在后端写CrossOrigin。原因是写在后端的跨域配置会直接允许所有来源安全性比较差而且如果你前后端联调时用的是同一个代理路径也不会报跨域错。以 Vue CLI 为例在vue.config.js里加module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这意味着你在前端请求/api/elder/list开发服务器会把请求转发到http://localhost:8080/api/elder/list浏览器的同源策略就不会拦截。生产环境里 Nginx 的proxy_pass承担了同样的角色所以后端代码里不需要额外处理跨域。如果你非要后端加跨域配置注意要处理 OPTIONS 预检请求。JWT 拦截器里会自动放行 OPTIONS否则前端带自定义 header 的请求会在预检阶段直接被拦截状态码变成 401。4.2 权限系统的常见坑JWT 过期与拦截器放行JWT 过期之后怎么办这是被问烂了的问题但确实很多人处理不好。最简单的方案是token 有效期设 24 小时过期后前端收到 401 就强制重新登录同时把本地 token 清掉。这个方案体验不算好但逻辑简单适合毕设。如果你想让体验更顺滑可以考虑加一个“刷新令牌”逻辑后端再生成一个 refresh_token有效期 7 天前端在主 token 过期后拿 refresh_token 去换新 token。但这样需要再写一个接口、再维护一个 token 存储表复杂度和工作量都会上升。我的建议是如果时间充裕可以做如果时间紧张就老老实实用“过期重新登录”。另外一个容易犯的错拦截器放行静态资源和登录接口的路径写错了。比如你的 swagger 地址是/doc.html如果你没有把它排除在拦截范围外请求就会 401。记得在注册拦截器的时候把 swagger 相关的几个路径都加进白名单。4.3 时间字段的时区与格式化问题很多人做项目时会遇到一个问题数据库存的时间是正常的但接口返回给前端后时间多了 8 个小时或者少了 8 个小时。这几乎都是时区问题。MySQL 连接串里一定要带serverTimezoneAsia/Shanghai这是第一步。第二Spring Boot 的 Jackson 也要设置时区也就是前面说的time-zone: GMT8。两个都设了前后端的时间就一致了。另外MyBatis-Plus 自动填充创建时间时有人直接在字段上写TableField(fill FieldFill.INSERT)但忘了实现MetaObjectHandler导致创建时间永远为空。这个坑也很常见需要单独建一个配置类Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }4.4 前端表格、日期组件与图表的常见问题Element UI 的表格el-table和数据更新有著名的“不刷新”问题。比如你删除了一行但表格视图还是显示旧数据。解决办法通常是在拿到新数据后给表格重新赋值一个数组引用或者给el-table加一个:key值数据变化时更新 key 强制重新渲染。具体来说删除操作后调用查询接口拿最新的分页数据再用this.tableData [...res.data.records]这样的方式赋值尽量不要原地修改数组。日期选择器也是重灾区。el-date-picker默认的v-model绑定的值类型是 Date 对象如果你直接提交到后端后端接收RequestBody里的字符串和 Date 可能对不上。解决方案是在组件上设置value-formatyyyy-MM-dd这样绑定的值直接就是格式化字符串和后端字段完美匹配。ECharts 图表如果放在el-tabs或者隐藏的el-card里第一次加载的时候往往显示不出图因为容器宽度为 0图表初始化失败。解决办法是每次切换到显示该图表的时候调用chart.resize()或者干脆等容器完全渲染后再init。常见的写法是this.$nextTick(() { if (this.chart) { this.chart.resize() } })4.5 MyBatis-Plus 的雪花ID与逻辑删除注意事项MyBatis-Plus 默认使用雪花算法生成主键 ID这个 ID 是 Long 类型。问题在于 Java 的 Long 类型最大值远超 JavaScript 的 Number 安全范围如果前端直接把 ID 当作数字处理精度会丢失。典型症状是你查出一条数据点编辑按钮提交后后端却提示“没有找到该记录”因为 ID 已经变成了一个错误的数字。解决办法是在实体类的主键字段上加注解序列化成字符串TableId(type IdType.ASSIGN_ID) JsonSerialize(using ToStringSerializer.class) private Long id;加了JsonSerialize(using ToStringSerializer.class)之后前端拿到的 id 就是字符串“1234567890123456789”精度不会丢。逻辑删除方面除了配置全局的logic-delete-field之外还要注意一个问题如果你在表中没有定义deleted字段但配置里写了逻辑删除MyBatis-Plus 做查询时会自动在 SQL 里加where deleted 0一运行就报“Unknown column”所以表里一定要有deleted字段再启用逻辑删除。5. 答辩讲解与二次开发扩展思路5.1 答辩时怎么讲这个项目才能拿高分很多学生写完代码答辩时却讲不清楚白白浪费好项目。我建议按照这条逻辑线来讲先讲业务痛点。“传统养老公寓很多还停留在纸质台账阶段老人的健康数据散落在各个护理记录本里家属想了解老人情况只能打电话询问管理层想看整体入住和健康状态也缺乏数据支撑。”这句话一说出来老师就会觉得你有业务思考。接着讲系统架构。说明前后端分离的架构后端 SpringBoot 负责业务逻辑和数据持久化前端 Vue 负责交互和展示数据库 MySQL 存储核心业务数据JWT 负责身份认证。然后重点讲亮点模块。依次演示登录后进入管理员首页看到床位利用率、老人人数、健康异常提醒的统计卡片点进老人档案新增一名老人自动分配床位进入健康档案给老人录入一条血压血糖数据立刻在趋势图里看到变化进入护理任务模块创建一个护理任务并分配给护理员状态从“待执行”变为“已完成”。这一套流程下来项目的信息量就很充足了。最后准备至少三个“技术细节追问”的答案。老师可能会问“分页怎么做” 答MyBatis-Plus 的 Page 插件底层基于拦截器在查询 SQL 前拼装 limit 语句一次性查出 total 和 records。“权限控制怎么做的” 答登录后 JWT 中写入用户角色后端拦截器解析 token对不同角色的接口做访问控制前端路由守卫控制页面跳转。“密码存在数据库里是明文吗” 答BCrypt 加密存储每次登录比对的是加密后的密码哈希值即使数据库泄露也无法反查明文。5.2 二次开发方向如何从毕设项目升级为作品集项目如果你做完这个项目还精力充沛想让它从“毕设作品”升级成“简历重点项目”可以沿着以下几个方向继续扩展第一接入物联网设备的数据。现在是“大健康”主题最自然的扩展就是对接智能手环、血压计等设备的数据上报接口。不用真的买硬件你可以用模拟数据接口代替后端写一个定时任务或者模拟客户端每隔一段时间向系统推送一条心率、血压数据前端实时展示。这个扩展能体现你对物联网数据采集的理解。第二做健康告警与消息通知。当健康指标超过阈值比如血压超过 160/100系统自动生成告警记录并通过 WebSocket 向前端推送通知也可以接入短信或微信模板消息向家属发送提醒。WebSocket 是很多老师喜欢追问的技术点能讲清楚“服务端主动推送”和“HTTP 轮询”的区别会加分不少。第三增加数据大屏。管理员首页放一个大屏可视化页面展示公寓总览、床位分布、老人年龄段分布、健康状态统计、护理任务完成率。技术实现可以用 ECharts 的多个图表拼接也可以通过 DataV 这类组件库快速搭建。大屏页面在答辩现场演示的视觉冲击力极强。第四家属端小程序或 H5。Vue 项目里拆出一个独立的 H5 端或者在微信小程序里重新实现家属查询功能。这个扩展的主要价值是让系统从“内部管理系统”延伸到“对外服务平台”业务闭环更完整。我个人在实际带项目的过程中发现凡是能把“健康数据闭环”这条线讲清楚的学生答辩效果普遍都不错。因为这个项目真正值钱的地方不在于做了多少页面而在于你把“养老公寓”这个传统场景和“健康管理”这个时代需求结合起来了。所以我的建议是与其贪多求全堆功能模块不如把老人档案、健康记录、趋势分析、护理任务这四块做深做透让每一处代码都能说出设计理由。最后再分享一个小技巧演示之前一定要准备好一份“演示数据”。什么是好的演示数据有完整姓名、有连续 7 天健康记录、有不同状态的护理任务的数据。我见过太多人在演示时现场输入“张三”、“李四”然后从空列表开始操作效果差一大截。提前在数据库里插入两三位老人的完整数据演示时直接打开趋势图说“这是张奶奶最近一周的血糖变化”说服力完全不一样。项目写得好也要讲得好这两件事同等重要。