1. 项目背景与整体设计思路
1.1 这个毕设到底在做什么
社区空巢老人照管系统,这个词乍一看有点长,但拆开就很好理解:面向城市社区场景,用一套信息化的工具,把独居、空巢老人的基础档案、健康状态、走访记录、家属联系方式这些散落的信息统一管起来,让社区工作人员不再靠Excel和纸质台账过日子。
我在帮不少同学梳理毕业设计时发现,这类题目的痛点是相通的。你说它复杂吧,它本质上就是几套增删改查的叠加;你说它简单吧,如果只做一堆孤立的CRUD页面,答辩时被老师追问业务闭环,又会被堵得说不出话。所以这个项目的关键点不在代码量,而在你是否把“照管”这件事讲明白了——谁在管、管什么、怎么发现问题、怎么通知家属、怎么留痕。
从题目自带的“部署教程+可完整运行源码+数据库”这几个关键词也能看出,这类毕设的交付物不只是源码,而是一套别人能拿过去直接跑起来的东西。也就是说,你不仅要写代码,还要写清楚怎么把环境搭起来、数据库怎么导入、前端怎么启动,这也是很多零基础同学最容易卡住的地方。
1.2 核心用户与功能模块拆解
做这个系统之前,先别急着写代码,你得像项目经理一样盘一遍用户。这个系统按我惯用的思路,会拆成三类角色:系统管理员、社区工作人员、老人家属。管理员负责账号分配和数据兜底,工作人员负责日常走访、健康记录、服务工单的维护,家属可以查看自己老人的基础照管状态(有些毕设里家属端不做登录,通过工作人员的分享链接或导出报告代替,这个看你课题要求)。
对应的功能模块大概有六个:老人档案管理、健康信息管理、走访/服务记录管理、提醒事项管理、数据统计看板、系统管理(用户/角色/菜单)。把这六个模块列出来,你的系统骨架基本就立住了,剩下的都是往里面填血肉。
我特别建议在健康信息管理里加一个“健康异常标记”字段。很多同学只做体检数据、血压血糖的录入和展示,但社区工作人员真正需要的是一眼看出这个老人今天是否需要重点关注。有了异常标记,再配合一个“待回访列表”,整个系统的业务感马上就出来了,答辩时老师也会觉得你确实思考过实际场景,而不是只会写CRUD。
1.3 技术选型为什么这么定
毕业设计的技术栈,我的建议永远是“稳妥优先,别玩花活”。这套系统我推荐用最经典的前后端分离组合:后端Spring Boot 2.7.x + MyBatis Plus + MySQL 8.0,前端Vue 2 + Element UI。Spring Boot让项目结构和依赖管理变得非常简单,不需要像早期SSH那样写一大堆XML配置;MyBatis Plus能把单表的增删改查压缩到极致,适合毕设这种以业务功能展示为主、没有复杂SQL优化的项目;Vue 2 + Element UI则是因为网上资料多,遇到问题搜一下就有一堆解决方案。
为什么不用Spring Boot 3?因为Spring Boot 3强制JDK 17+,部分老电脑或学校机房环境装的是JDK 8,直接踩版本坑。为什么不用Vue 3?不是不行,是Composition API和Element Plus的学习成本比Vue 2高一截,对于需要快速把功能堆出来的毕设来说,Vue 2的Options API配合Element UI写起来更顺手,模板示例也多。记住一个原则:毕业设计的核心是完整度,不是技术新度。
2. 数据库设计与核心表结构
2.1 数据库设计的原则与思路
有些同学一上来就建二三十张表,表之间外键拉满,觉得自己设计得很“全面”,结果后期自己写SQL都绕晕了。数据库表设计要跟着页面走,页面上要展示哪些字段,表里就落哪些字段;反过来,页面展示需要的字段如果表里没有,再考虑是加字段还是加关联表。
这个系统的表设计,我的方案是七张核心表加两张辅助表。核心表包括:用户表(admin/staff两类账号)、老人信息表、家属表、健康记录表、走访/服务记录表、提醒事项表、通知公告表。辅助表包括:字典表(存放性别、健康异常级别、服务类型这类枚举值)和操作日志表。这个量级的表对于毕设来说刚好,太少了撑不起功能展示,太多了维护成本高,老师问起来你还要花大量时间解释每张表的作用。
关于外键,我的建议是逻辑外键,不要物理外键。什么意思?比如老人信息表里的社区字段,你不需要真的去建一个外键约束,只需要在逻辑维护的时候保证数据一致即可。物理外键在删除数据、批量导入的时候特别容易报错,而毕设项目里数据量不大,逻辑外键完全够用,还能让你的SQL活很多。
2.2 老人信息表与健康记录表的核心字段
老人信息表是整个系统的数据底座,字段设计直接决定后面功能好不好写。我列一下核心字段,你们参考时根据自己的课题调整即可。
CREATE TABLE `elder_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(32) NOT NULL COMMENT '姓名', `gender` tinyint(1) DEFAULT '1' COMMENT '性别 1男 2女', `age` int(11) DEFAULT NULL COMMENT '年龄', `id_card` varchar(18) DEFAULT NULL COMMENT '身份证号', `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `address` varchar(255) DEFAULT NULL COMMENT '居住地址', `community` varchar(64) DEFAULT NULL COMMENT '所属社区', `health_status` varchar(16) DEFAULT '正常' COMMENT '健康状态 正常/关注/高风险', `emergency_contact` varchar(64) DEFAULT NULL COMMENT '紧急联系人', `emergency_phone` varchar(20) DEFAULT NULL COMMENT '紧急联系电话', `elder_type` varchar(32) DEFAULT NULL COMMENT '老人类型 空巢/独居/失能', `remark` varchar(500) DEFAULT NULL COMMENT '备注', `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='老人信息表';健康记录表主要记录每次走访或体检时老人的血压、血糖、心率等指标,然后关联老人ID。这里有个细节:健康指标的计量单位一定要写清楚,血压的mmHg、血糖的mmol/L,不然以后做数据统计时数值单位对不上,出来的图表没法看。另外建议加一个“记录来源”字段,标注是系统录入还是家属填写,便于追溯。
2.3 初始化数据与常见坑点
数据库脚本除了建表语句,一定要带上初始化数据。至少要有:一个管理员账号(admin / admin123,密码建议MD5或BCrypt加密后的值)、一个工作人员账号、三五个老人演示数据,以及对应的健康记录和走访记录。这样部署方导入SQL后,一打开系统就能看到数据,不用自己手动造。很多所谓的“完整源码”数据库是空的,演示效果大打折扣,也显得开发者不专业。
导入SQL时容易出的问题有两个。第一是字符集,建库语句里最好明确指定utf8mb4:CREATE DATABASE eldercare_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;,不然你中文数据导入MySQL后全是问号。第二是SQL执行顺序,如果在MySQL客户端里直接粘贴执行包含多个表的脚本,要确保表之间没有循环引用,否则会报“无法创建外键”的错误。我提供的逻辑外键方案,从根源上就规避了这个坑。
3. 后端核心实现与源码导读
3.1 项目目录结构,拿到源码先看哪里
拿到一份完整源码,最忌讳的就是Ctrl+F到处找文件,找不到就慌。你要先看目录结构。这套系统后端用的是标准的Maven结构:
src/main/java/com/eldercare ├── common # 通用工具类、统一返回结果、异常处理 │ ├── Result.java │ ├── ResultCode.java │ └── GlobalExceptionHandler.java ├── config # 配置类,包括跨域、MyBatis Plus分页插件 ├── controller # Controller层,接收前端请求 ├── entity # 实体类,对应数据库表 ├── mapper # MyBatis Plus的Mapper接口 ├── service # Service接口 └── service/impl # Service实现类拿到项目后,第一步先看application.yml,它包含了端口、数据库连接信息、MyBatis Plus日志配置等最关键的运行参数。第二步看common/Result.java,弄明白接口的统一返回结构是什么。第三步找一个完整的Controller,从入口开始捋一遍请求是怎么走的。这三步走下来,你对这个项目的理解能达到70%。
3.2 登录鉴权与拦截器的实现逻辑
登录鉴权如果你用Spring Security,光各种配置和过滤器链就能折腾一整天,对毕设来说性价比不高。我更推荐用JWT + 拦截器的方案,既好讲清楚原理,代码量也不大。
思路是这样的:用户拿着账号密码请求登录接口,后端校验通过后,用JWT工具类生成一个token,把用户ID和用户名塞进去,返回给前端。前端每次请求都在header里带上Authorization: Bearer token。后端写一个拦截器,拦截所有/api/**请求(放行登录接口),在拦截器里解析token,解析失败就返回401。
public class JwtInterceptor implements HandlerInterceptor { @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.isEmpty() || !JwtUtil.validateToken(token.replace("Bearer ", ""))) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}"); return false; } return true; } }这里有个细节值得在答辩时讲清楚:为什么要放行OPTIONS请求。因为当前端和后端在不同的端口时,比如前端8080、后端9090,浏览器跨域访问会先发一个预检请求,如果你把OPTIONS也拦截了,前端真实请求根本到不了后端,控制台报的全是跨域相关错误。这个问题我们在第5章部署时会再次遇到。
3.3 老人档案模块的增删改查是怎么写的
用MyBatis Plus写单表CRUD,是真的省力。实体类继承BaseEntity后,Mapper接口只需继承BaseMapper<ElderInfo>,Service实现类继承ServiceImpl,你甚至不需要写一行SQL,基础方法就全有了。
@RestController @RequestMapping("/api/elder") public class ElderInfoController { @Autowired private ElderInfoService elderInfoService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, String name, String healthStatus) { LambdaQueryWrapper<ElderInfo> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.hasText(name)) { wrapper.like(ElderInfo::getName, name); } if (StringUtils.hasText(healthStatus)) { wrapper.eq(ElderInfo::getHealthStatus, healthStatus); } wrapper.orderByDesc(ElderInfo::getId); Page<ElderInfo> page = elderInfoService.page(new Page<>(pageNum, pageSize), wrapper); return Result.success(page); } @PostMapping("/save") public Result save(@RequestBody ElderInfo elderInfo) { elderInfoService.saveOrUpdate(elderInfo); return Result.success(); } @DeleteMapping("/delete/{id}") public Result delete(@PathVariable Long id) { elderInfoService.removeById(id); return Result.success(); } }注意我把新增和修改整合成了一个saveOrUpdate接口。为什么这么设计?因为前端页面上,新增弹窗和编辑弹窗长得一模一样,提交时前端只需要判断一下当前有没有ID,有ID就带上ID请求同一个接口,后端通过ID是否存在决定插入还是更新。这样做可以减少前端冗余代码,也避免了两套接口的维护成本。这个设计同样可以在答辩时作为经验点讲。
3.4 健康异常提醒的核心筛选逻辑
前面提到的“待回访列表”听起来很有业务感,但实现其实就是一个条件查询。我在健康记录表里加了一个abnormal_flag字段,当工作人员录入健康记录时,如果某项指标超出正常范围,就把它标记为1。然后写一个查询接口,关联老人信息表和健康记录表,过滤出近7天内有过异常标记的记录,同时排除掉状态已经是“高风险”或“已处理”的老人。
用SQL表达就是先把健康记录表按老人ID分组取最新的一条,保留异常标记为1的,再和老人信息表关联。这个查询用MyBatis Plus的LambdaQueryWrapper也能做,但字段多的时候直接写SQL注解更清晰。说到底,业务功能的亮点不需要复杂的算法,只要你的筛选逻辑贴合实际管理工作流,效果就很好。
4. 前端页面与交互设计
4.1 前端项目结构与路由设计
前端项目我用的是Vue 2 + Element UI + Axios。目录结构如下:
src ├── api # 接口请求封装,按模块拆分文件 ├── assets # 静态资源 ├── components # 公共组件 ├── layout # 整体布局,包括侧边栏、顶栏 ├── router # 路由配置 ├── store # Vuex状态管理 └── views # 页面视图 ├── elder # 老人档案 ├── health # 健康记录 ├── visit # 走访记录 ├── dashboard # 数据看板 └── system # 系统管理路由设计上,我用的是静态路由 + 动态菜单。也就是说,动态菜单是用后端返回的菜单列表渲染出来的,路由表则提前在router里全部配好,只是通过菜单的isShow字段控制是否显示。为什么这么做?因为真正的动态路由需要在用户登录后根据权限addRoute加载,逻辑比较复杂。毕设阶段,用菜单显隐控制就能达到“不同角色看到不同功能”的展示效果,也够用了。
4.2 老人档案列表页的核心代码
列表页是后台管理系统出现频率最高的页面形态,我直接贴一段最核心的表格 + 分页代码,你们参考时替换成自己的接口即可。
<template> <div> <el-form :inline="true"> <el-form-item label="老人姓名"> <el-input v-model="query.name" placeholder="请输入姓名" clearable /> </el-form-item> <el-form-item label="健康状态"> <el-select v-model="query.healthStatus" placeholder="请选择" clearable> <el-option label="正常" value="正常" /> <el-option label="关注" value="关注" /> <el-option label="高风险" value="高风险" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="loadData">查询</el-button> </el-form-item> </el-form> <el-table :data="tableData" border stripe> <el-table-column prop="name" label="姓名" width="90" /> <el-table-column prop="age" label="年龄" width="60" /> <el-table-column prop="community" label="所属社区" /> <el-table-column prop="healthStatus" label="健康状态"> <template slot-scope="scope"> <el-tag :type="scope.row.healthStatus === '高风险' ? 'danger' : 'warning'"> {{ scope.row.healthStatus }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="200"> <template slot-scope="scope"> <el-button type="text" @click="openEdit(scope.row)">编辑</el-button> <el-button type="text" @click="showDetail(scope.row)">详情</el-button> <el-button type="text" class="danger" @click="deleteElder(scope.row.id)">删除</el-button> </template> </el-table-column> </el-table> <el-pagination @current-change="handlePageChange" :current-page="query.pageNum" :page-size="query.pageSize" :total="total" layout="total, prev, pager, next" /> </div> </template>页面本身没什么难度,重点在于接口封装。我习惯把每个模块的请求单独放到src/api/elder.js里,统一使用一个axios实例,baseURL指向/api,然后在Vue项目的根目录配置一个代理,把/api转发到后端地址。这样做的好处是,开发时后端端口怎么变,前端代码完全不用动,只要改代理配置就行,部署时也能灵活切换。
4.3 前后端联调、跨域与代理配置
前后端联调最大的敌人是跨域,但问题本身并不难解决。开发阶段我推荐用Vue CLI的devServer.proxy解决,而不是在后端代码里写@CrossOrigin。因为@CrossOrigin是后端允许跨域访问,生产环境部署时如果前后端同域,反而多了一层奇怪逻辑;用前端代理则只是开发阶段有效,部署后无残留。
// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } } };这样配置后,前端发出的/api/elder/list请求,在开发环境会被代理到http://localhost:9090/api/elder/list。注意后端Controller的映射是@RequestMapping("/api/elder"),也就是说,代理只转发了域名和端口,路由路径在后端仍然是完整的/api/**,两边统一带/api前缀是约定,别在中间随意增删前缀。
5. 本地部署教程(从零到可完整运行)
5.1 环境准备与版本对应关系
部署这个项目,环境这一关能拦住一半以上的人。不是说安装环境本身多难,而是版本之间相互不兼容,装完了启动时才报错,排查起来没有头绪。我列一下这套系统最稳妥的环境组合,你们照抄即可。
开发工具和后端环境方面:JDK 1.8(这是Spring Boot 2.7的默认适配版本)、Maven 3.6.x、IntelliJ IDEA 2020及以上(社区版也可)。MySQL建议8.0以上,5.7也能跑,但一些SQL语法和驱动配置会有差异,用8.0省事。前端环境:Node.js 14到16之间的版本,npm会随Node自动安装。如果Node版本太高,比如18、20以上,部分旧依赖在编译时可能报错。
数据库客户端推荐Navicat,学校机器普及率高,可视化操作简单。如果用的DataGrip或MySQL Workbench也行,核心操作都是:新建数据库、运行SQL脚本、查看表数据。
5.2 后端部署详细步骤
后端从零到启动,我用五步总结。先别急着点绿三角运行,按顺序检查一遍,能避免大量无谓报错。
第一步,导入项目。打开IDEA,选择File -> Open,选中项目根目录的pom.xml,导入方式选“作为Maven项目”。IDEA会开始下载依赖,这一步需要联网,如果等了很久没动静,检查IDEA的Maven配置是否指向了国内镜像,可以用阿里云Maven仓库镜像。
第二步,创建数据库并导入SQL脚本。在Navicat里新建数据库,名字和application.yml里的database参数保持一致。然后右键数据库,选择“运行SQL文件”,选中项目根目录下的sql/eldercare.sql,执行完毕后刷新,能看到所有表和初始化数据。
第三步,修改配置文件。打开application.yml,逐一核对端口号、数据库地址、用户名、密码。重点关注server.port和spring.datasource.url,数据库URL里注意时区参数,MySQL 8.0必须带serverTimezone=Asia/Shanghai,否则会报时区相关的错误。
第四步,启动入口类。在项目结构里找到EldercareApplication.java,右键运行。启动成功的标志是控制台出现Spring Boot的启动日志,并且最后的启动时间旁边没有ERROR字样。初次启动如果有报错,90%是数据库连接的问题,按第三步继续排查。
第五步,验证接口。浏览器访问http://localhost:9090/api/elder/list,如果返回JSON数据,说明后端已经就绪。如果返回404,别慌,大概率是接口路径写错了,去Controller里核对一下@RequestMapping注解与数据库数据。这里要明确一点:后端启动没有任何页面反馈是正常的,因为这是前后端分离项目,页面得靠前端跑。
5.3 前端部署详细步骤
前端部署比后端简单,但坑也不少。同样是五步。
第一步,用IDEA或VS Code打开前端目录elder-web。如果你是第一次打开,项目里还没有node_modules文件夹,别急,这是正常的。
第二步,在项目根目录打开终端,执行npm install。这一步会安装所有依赖,根据网络速度通常要两三分钟到十几分钟不等。如果中途报各种红字错误,往下看5.4节的处理方法。
第三步,确认vue.config.js里的代理配置正确,然后执行npm run serve。启动成功后,控制台会显示本地访问地址,一般是http://localhost:8080。
第四步,浏览器打开http://localhost:8080,用初始化数据里的管理员账号登录,如果能进去看到数据面板,说明前后端对接成功。
第五步,确认登录接口能通。打开浏览器F12开发者工具,点一下登录按钮,看Network面板里登录请求的响应状态。200说明正常,401说明token或账号不对,500说明后端报错,根据状态码再定位问题。
5.4 我踩过的部署Bug汇总
第一个Bug:npm install极慢或卡住。解决办法是先设置npm淘宝镜像源:npm config set registry https://registry.npmmirror.com,再重试。如果项目里某些包被墙或版本冲突,删除package-lock.json后再install。
第二个Bug:MySQL连接报Access denied for user 'root'@'localhost'。这通常不是你密码错了,而是MySQL授权的host匹配问题。最简单的解决办法是用root账号重新授权一下(选做,授权前确认你清楚自己在做什么),或者直接在Navicat里新建一个专门给项目用的账号和密码,然后把application.yml改成新账号。
第三个Bug:后端启动成功,前端页面死活加载不出数据。先确认前端代理是否生效,方法是在Network面板看请求实际发到了哪个地址。如果请求直接发到了http://localhost:8080/api/...,并且报404,说明代理可能没重启。Vue.config.js修改后必须重启npm run serve,改动才会生效。这个坑我踩过不下三次,每次都是改完代理忘了重启。
第四个Bug:Element UI 的图标不显示。通常是main.js里没有引入图标库,或者主题文件路径不对。处理方案是检查main.js是否引入了element-ui/lib/theme-chalk/index.css和element-ui/lib/theme-chalk/fonts目录静态资源。必要时把字体文件放到public目录下。
6. 答辩准备和代码二次扩展建议
6.1 功能演示怎么讲才不冷场
答辩时间一般只有五到十分钟,别照着页面从头到尾念一遍。我建议按“业务痛点 → 核心流程 → 亮点功能”的顺序来。开场先花30秒讲清楚背景:社区空巢老人数量上升,纸质台账管理效率低、发现问题不及时,所以做了这个系统。然后演示一个完整闭环:新增老人档案 → 录入健康记录并触发异常标记 → 在待回访列表看到老人 → 生成走访记录 → 通知家属。走通这一条线,比你罗列十个功能页面都管用。
因为评委老师真正关心的,是你有没有把需求吃透、数据怎么流转、遇到问题怎么思考。业务闭环能回答第一个问题,数据库表和接口能回答第二个问题,而你准备的那一串踩坑记录,就是回答第三个问题的弹药。
6.2 老师最爱问的几个问题
根据历年答辩反馈,关于这套系统的高频提问集中在三块。第一块是权限控制:“不同角色的权限是怎么实现的?”你只要回答用户表有role字段,后端拦截器校验角色,前端菜单用v-if配合角色字段控制显隐,就够了,不要扯到RBAC太深。
第二块是数据安全:“管理员密码是明文存储的吗?”如果你用了MD5或BCrypt,就如实说明并展开两句;如果没做加密,答辩前最好加上,这属于安全性问题,被问到就露怯了。
第三块是设计理由:“为什么健康记录不直接建在老人表里?”这个问题也是送分题,你回答“因为健康记录和老人是一对多关系,一个老人会有多条历史健康记录,拆表方便做历史趋势和数据分析”,老师一般不再追问。
6.3 代码还能怎么扩展出亮点
如果做完上述功能还有余力,我推荐按难度从低到高扩展设计。低难度是给老人档案模块加一个批量导入Excel功能,使用EasyExcel或POI,一般在刷新页面后看不到现象,但老师现场让你演示时效果出人意料的好。
中难度是加一个ECharts可视化看板,把老人的年龄段分布、健康状态占比、各社区老人数用饼图柱状图展示出来。ECharts这种数据可视化内容天然有视觉冲击力,演示的时候打开一个图表大盘,比展示十个表格管用得多。前端实现也不复杂,Vue中引入echarts组件,在mounted钩子里请求后端统计接口,把数据塞进setOption里即可。
高难度是接入RabbitMQ或定时任务,做每日健康提醒。比如每天早上九点自动给社区工作人员推送今天的走访提醒清单。这个功能能展示你对异步处理的理解,但如果你之前没接触过消息队列,不建议临答辩前突击引入,容易翻车。
7. 写在最后的几点体会
做这类照管系统的毕设,我个人的感受是,最终答辩成绩往往不取决于你的技术有多深,而取决于你能不能把自己的设计讲成一个让人信得过的故事。技术方面Spring Boot加Vue这套组合管够,真正拉开差距的是你说清楚了这个系统能为真实的社区工作带来什么变化。
如果你拿到源码后感觉一团乱麻,我的建议是先别碰代码,花一下午画三张图:一张角色用例图、一张功能结构图、一张核心业务的流程图。三张图画完,整个项目在你脑海里就会长成一个可以随意进出的房子,而不是一堆乱七八糟的房间。之后无论改代码、写论文、做PPT、应付答辩,你都有清晰的脉络。
最后再分享一个小技巧:部署时一定要把application.yml里的数据库密码和你本机的MySQL密码保持一致,这个看起来最不起眼的地方,卡住的人其实最多。你先把环境跑通,再回头研究源码,整个过程会顺畅很多。