☰
基于SpringBoot+Vue的社区养老服务平台设计与实现全流程解析
2026/10/13 13:14:25 网站建设 项目流程

前阵子帮一个在校生做项目评审,题目就是“基于SpringBoot+Vue的社区养老服务平台管理系统设计与实现【Java+MySQL+MyBatis完整源码】”。那位同学一开始挺自信,数据库建了二十多张表,接口也写了将近两百个,可等我打开系统一看,登录页倒是出来了,点进去之后页面跟数据完全对不上。问题不是代码量少,而是对“社区养老”这个业务模块的边界没想清楚。

这篇文章我不打算逐行晒源码,而是把拿到这个题目后,从需求分析、数据库表设计、后端接口、前端页面,再到部署冒烟测试的完整思路走一遍。目的是让你在写代码之前,先答得上来系统到底管什么、为什么这样建表、接口和页面怎么对接上。思路顺了,代码只是时间问题。

1. 先搞清楚:这个系统到底要管什么

1.1 别急着建表,先把“用系统的人”摆出来

很多同学拿到题目第一反应是打开数据库工具建表,这是最容易翻车的做法。社区养老服务平台的核心不是“报一个老年人口数字看板”,而是要让三类人通过同一个系统协作起来。

我从需求层面把角色分成三类,后面所有功能和表都是围着这三类人转的:

角色核心诉求使用方式
平台管理员管人、管服务、看数据。维护老人档案,审核工单,分配服务人员,统计服务完成情况PC后台管理端
服务人员/护工接收工单,查看老人基础信息和注意事项,上门服务后填报结果PC后台或移动H5
老人及家属查询服务进度、健康记录、历史工单以查看为主,多数场景走家属代办

如果你把这三类人的对话串成一条线,业务其实很清晰:管理员录入老人档案,老人产生服务需求,管理员创建服务工单,护工接单并上门,服务完成后生成回访记录和健康数据。这就是一个典型的服务流转闭环,和外卖平台的“建店—下单—接单—配送—评价”模型很像,只是把商品换成了助餐、助洁、助医等养老服务。

1.2 从业务痛点倒推功能清单

社区养老系统的痛点,拆成三层来看:

第一层是基础信息管理。老人的联系方式、紧急联系人、基础疾病、自理能力、居住地址,原来都是纸质台账,查询和更新都很痛苦。系统要解决的核心问题就是把档案电子化,并且能快速检索。

第二层是服务流程管理。服务不是“今天有人上门就行”,得有人创建工单、有护工接单、有服务开始时间和完成时间、有回访记录。没有流程管理,养老服务就停留在口头安排,出了纠纷也说不清楚。这里需要一个完整的工单状态机。

第三层是数据辅助决策。街道或者社区想知道的不是单个人的情况,而是“这个月助餐服务做了多少次”“有多少老人健康数据连续三个月在血压高位”。系统要能做基础统计,支撑后续的补贴发放、服务资源调配。

1.3 划清项目边界,不要给自己挖坑

任何“设计与实现”类项目,最怕的不是功能少,而是功能多到没法收尾。我这个标题下的系统,核心闭环就四个模块:老人档案管理、服务工单管理、健康记录管理、系统权限管理。外部物联网设备对接、移动端App、GPS定位、视频通话、在线支付这些,都不要放进第一版。

不是说这些功能没用,而是你要在答辩时讲清楚“为什么不做”。我通常建议在数据库里预留扩展位(比如健康记录表预留一个device_id字段),但界面上不展示。这样既保住了开发进度,也显得你考虑过扩展性,而不是压根没想到。

2. 技术选型不跟风:这套组合为什么这么搭

2.1 SpringBoot,解决的是“配置地狱”问题

题目指定SpringBoot不是没有道理的。前几年用SSM做同类项目,光applicationContext.xml、spring-mvc.xml、web.xml就能让新人纠结一周。SpringBoot的核心价值是用自动配置把大部分模板代码沉淀掉,让你把精力放在业务逻辑上。

但有两个容易被忽略的关键点。

第一是版本匹配。如果你的JDK还是8或11,不建议用SpringBoot 3.x,因为3.x强制要求JDK17,很多本地环境和教学模板还是1.8跑着。稳妥组合是JDK8 + SpringBoot 2.7.x,兼容性最好,网上资料最多,遇到问题能快速搜到答案。

第二是不能完全依赖自动配置。数据库连接、端口、MyBatis的mapper扫描路径、文件上传大小限制,这些依然要在application.yml里明确写上。很多同学“SpringBoot项目启动失败”的报错,其实就是漏了数据源参数。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/community_care?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.communitycare.entity

2.2 Vue + Element UI,是后台管理项目的现实之选

前端技术栈为什么要用Vue?因为社区养老平台本质上是一个“管理后台”,核心页面是表格、表单、弹窗、统计卡片。Vue的单文件组件和响应式数据绑定,让这种CRUD密集型页面开发效率非常高。

这里要提醒版本问题:如果题目只写“Vue”,市面上大量课程和模板还是Vue 2.6 + Element UI的组合,而不是Vue 3 + Element Plus。Vue2和Vue3在生态上差别非常大,很多同学下载一个Vue3的模板,结果照着一本Vue2教材写,半天跑不通。

我给你的建议是:先确认你的基础。如果你跟着的教程是Vue2,项目就用Vue2;如果你已经会Vue3的setup语法,就选Vue2项目中常用的组件库。核心目的不是追新,而是让前后端能顺利联调完毕。

技术栈版本优点注意点
Vue 2.6 + Element UI案例最多、组件踩坑记录多,适合课程设计节奏2023年后官方已停止维护
Vue 3.2 + Element Plus组合式API更优雅,长期维护有保障部分组件用法与Element UI不同,模板少一些

2.3 MyBatis和MySQL,图的就是可控和简单

这套项目用MyBatis配合MySQL,最大的好处是SQL写法非常直观。MyBatis不像Hibernate那样有很强的对象关系映射,也不容易产生“你明明只查一条记录,它却给你发了一堆关联SQL”的尴尬情况。你自己写的SQL,出了性能问题自己也能看得懂。

有人会问:为什么不用MyBatis-Plus?我的回答是:如果题目允许,实际工程完全可以加,它能把单表CRUD代码缩短很多。但既然题目明确写了MyBatis,你核心的增删改查最好还是用原生SQL完成,至少在答辩时你能解释清楚“这个SQL是在查什么、为什么这么写”。结合使用也是可以的,但不要让你的项目里一眼望去全是Plus的LambdaQueryWrapper,而看不到一条自己写的SQL。

3. 功能模块拆解:从老人档案到服务工单

3.1 系统管理模块,先跑通权限闭环

我见过太多项目急着写老人列表,最后在登录权限上彻底翻车。系统管理这个模块虽然看起来不是业务亮点,但它是整个系统的地基。

推荐使用经典的RBAC模型:用户—角色—菜单三层关联。也就是一个账号可以被分配一个或多个角色,一个角色关联一组菜单权限。登录之后,后端根据用户ID查出角色,再根据角色查出菜单,前端根据返回的菜单树动态生成侧边栏。

这里有三个很实在的设计细节:

  • 菜单表里要有parent_id字段,用来做父子级菜单;一级菜单是“系统管理”“老人管理”“服务工单”“健康记录”,二级菜单才是具体的页面。
  • 按钮级的权限可以不做,但是菜单级权限一定要做。否则任何一个小管理员登录后看到的菜单和管理员一模一样,这个模块就没有意义了。
  • 密码不能明文存。至少用MD5加盐或者BCrypt加密。如果你在答辩的时候被问“数据库被拖走了怎么办”,这一步就是你的唯一防线。

3.2 老人档案模块,不是简单地写一个“老人表”

老人档案是整个系统的数据基础。字段上除了姓名、性别、身份证号、联系电话这些常规项,一定要有这几个业务字段:

  • 紧急联系人及电话:养老服务上门过程中一旦出现异常,必须快速找到家属。
  • 健康标签:例如“高血压、糖尿病、行动不便”,可以用逗号分隔的字符串存储。
  • 自理能力评级:从“完全自理”到“完全不能自理”,这直接影响服务工单的派单难度和时长。
  • 居住地址:要尽量写得具体,因为后面派单时,护工是按照区域接单的。

为什么要用逗号分隔字符串存储健康标签,而不是单独建一个“老人标签中间表”?因为本系统的核心使用场景是列表展示标签,而不是拿标签做复杂的组合检索。如果你建三张关联表来做这个,反而把简单问题复杂化,查询速度也没有提升。

3.3 服务工单模块,核心是状态机

服务工单是本系统业务价值最高的模块,建议把重点放在这里。工单从创建到归档,状态必须清晰:

状态值状态名称可操作角色说明
0待派单管理员工单刚创建,未分配给护工
1已派单管理员/护工已指定护工,等待接单确认
2服务中护工护工开始上门服务
3已完成护工服务结束,填写服务反馈
4已回访管理员管理员回访完成,工单归档

这五个状态背后有一个强烈的逻辑约束:状态不能随意跳转。比如“待派单”不能直接跳到“服务中”,必须先派单;“已完成”不能直接回到“待派单”。把状态流转规则写在Service层,前端只负责调用接口,不自己改状态。这样哪怕是用户频繁点击按钮、并发请求,数据也不会乱。

一个工单的基本流程是:管理员根据老人的需求(比如助餐、助洁)创建工单,选择服务类型,指定护工,护工接单后上门,完成后填写服务内容和耗时,最后管理员电话回访,记录满意度。这一套走下来,项目的核心难点和答辩亮点就都有了。

3.4 健康记录模块,做简单但要有分析价值

健康记录不要做得太重,因为社区养老系统的健康数据来源是护工上门测量或者老人家属手动填报,不是医院级别的数据。一张health_record表就够了:老人ID、收缩压、舒张压、空腹血糖、心率、记录日期、记录人。

这个模块可以做成一个“最近30天趋势图”。后端把查询参数传给前端,前端用图表组件画折线。比如一位老人的血压连续几次超过140/90,前端就提示“血压偏高”。这种分析不需要任何算法,就是普通的区间判断,但用来做项目展示非常讨巧,也呼应了“健康管理”的业务目标。

4. 数据库设计:九张表,怎么建才不返工

4.1 先画数据关系,再写建表语句

我在做这类项目时,习惯先确定表清单,再动SQL。这个系统我给出一套比较舒服的表结构,九张表,其中七张是核心主表,两张是关联表:

表名类型作用
sys_user主表系统登录账号,区分管理端账号和服务人员账号
sys_role主表角色定义,比如系统管理员、服务人员
sys_menu主表菜单权限定义
sys_user_role关联表用户和角色多对多关联
elder_info主表老人档案核心表
service_type主表服务项目目录,如助餐、助洁、助医、代办、陪伴
service_order主表服务工单主表,记录一次服务全流程
health_record主表老人的健康测量记录
service_follow主表工单回访记录表

为什么不额外建老人和家属的独立账户表?因为这个系统第一版面向的是管理员和护工,老人及家属更多是“被服务对象”而非主动登录用户。如果把老人也做成登录用户,就要牵扯注册、验证码、微信绑定,工作量直接上一个台阶。边界就是这么划出来的。

4.2 核心建表SQL,字段名和注释一样不能少

很多人建表时省注释,到写代码时到处翻。我建议用Navicat或MySQL Workbench建表时,把COMMENT写在字段后面。这里以service_order为例,你看一眼就明白工单表的设计要点:

CREATE TABLE service_order ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '工单ID', order_no VARCHAR(32) NOT NULL COMMENT '工单编号,如SG20250801XXXX', elder_id INT NOT NULL COMMENT '老人ID,关联elder_info', elder_name VARCHAR(50) NOT NULL COMMENT '冗余老人姓名,减少联表查询', service_type_id INT NOT NULL COMMENT '服务项目ID', service_type_name VARCHAR(50) NOT NULL COMMENT '冗余服务项目名称', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待派单 1已派单 2服务中 3已完成 4已回访', assign_user_id INT COMMENT '派单护工ID', assign_time DATETIME COMMENT '派单时间', start_time DATETIME COMMENT '服务开始时间', end_time DATETIME COMMENT '服务结束时间', service_address VARCHAR(255) COMMENT '服务地址', service_content VARCHAR(500) COMMENT '服务内容描述', feedback VARCHAR(500) COMMENT '护工服务反馈', satisfaction TINYINT COMMENT '回访满意度,1-5分', follow_user_id INT COMMENT '回访人ID', follow_time DATETIME COMMENT '回访时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0正常 1删除' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务工单表';

这里有一个很多教程不会教你的设计经验:工单表里冗余了elder_name和service_type_name。为什么不直接关联查?因为工单列表页需要显示老人姓名和服务项目名,如果每次都三表联查,SQL写起来麻烦,索引消耗也大。对于读写比例悬殊的系统(读多写少),冗余两个字段时间换空间,非常值。你只需要在创建工单和老人改名的接口里同步更新一下即可,这种低频维护成本完全可以接受。

4.3 逻辑删除和审计字段,从第一天就加上

我建议每张业务表都带上deleted、create_time、update_time三个字段。逻辑删除的意思是:用户点击“删除老人档案”时,不执行物理DELETE,而是UPDATE elder_info SET deleted = 1 WHERE id = ?。

这样做的原因有三个:

  • 老人档案是敏感数据,误删之后无法恢复,物理删除风险太大;
  • 工单表的外键指向老人档案,逻辑删除不会导致历史工单查询报错或关联缺失;
  • 答辩的时候,你可以解释这是“数据安全性的考虑”,属于加分项。

5. 后端实现要点:写接口前必须想清楚的几件事

5.1 统一返回结果,省去无数次前端联调

前后端分离项目,最忌讳每个接口返回结构都不一样。这个接口返回{code:1, msg:"ok"},那个接口返回{status:200, message:"success"},前端根本没法写通用逻辑。

我在项目中会定义一个Result<T>类,所有接口统一返回:

public class Result<T> { private Integer code; // 200成功,500业务异常,401未登录 private String message; // 提示信息 private T data; // 具体数据 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

配合@RestControllerAdvice全局异常拦截,业务里只需要抛出自定义异常,前端收到统一格式,弹message字段里的提示就行。这个设计能让你和前端配合时少吵很多架。

5.2 分页查询的完整链路,SQL和参数别搞混

老人列表、工单列表、回访列表,每个列表都需要分页。分页的链路是:前端传pageNum(第几页)和pageSize(每页多少条)到Controller,Controller转给Service,Service把参数传给Mapper,Mapper在SQL里用LIMIT offset, pageSize截取数据,最后返回一个包含list和total的PageResult。

很多人容易忽略的是offset的计算:offset = (pageNum - 1) * pageSize。如果用户在第2页,每页10条,就应该从第10条开始取10条,也就是LIMIT 10, 10。

前端拿到的总条数total,也需要在SQL里单独SELECT COUNT(*)查出来。这里千万不要用SELECT *然后去List.size()当总数,那样数据量一大必然出问题。

5.3 JWT登录鉴权,够用就好

登录方案我用JWT,不引入Spring Security或Shiro。理由是这个项目算上菜单权限,用拦截器加注解就能覆盖,引入重量级安全框架反而增加学习成本和配置复杂度。

具体实现思路是这样的:用户登录成功后,后端生成一个token,里面用userId作为自定义声明。前端每次请求在请求头里带上Authorization: Bearer xxx。后端写一个拦截器,放行登录接口,其余接口一律校验token,token有效就从里面取出userId,放到ThreadLocal或请求参数里供Controller使用。

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); Claims claims = JwtUtil.parseToken(token); if (claims != null) { request.setAttribute("userId", claims.get("userId")); return true; } } response.setStatus(401); return false; } }

有一点要注意:前端登录接口在白名单里,/api/login放行;但用户信息接口/api/user/info必须登录后才能拿到。如果放行得太宽,整个鉴权形同虚设。

5.4 MyBatis里最常见的几个坑

MyBatis的SQL写起来直观,但有几个点特别容易踩,我每次做项目都会盯一遍:

  • #{}和${}别混用。#{}是预编译占位符,会处理成?,安全防注入;${}是字符串替换,通常只用于动态排序列名,比如ORDER BY ${sortField}。传参数过滤条件一律用#{}。

  • 模糊查询的写法。MySQL里不能直接写LIKE '%#{keyword}%',会报错。正确写法是LIKE CONCAT('%', #{keyword}, '%')。

  • 批量插入用foreach。如果你在循环里逐条调用单条insert,几百条数据能卡到你怀疑人生。用MyBatis的<foreach>标签一次插入,效率差距是几十倍。

  • Map或实体类的param命名。多参数传递时建议用@Param注解,否则SQL里的参数名容易对不上。

6. Vue前端对接:别把后端数据直接塞进vuex

6.1 前端目录结构,一上来就按项目化组织

前端代码如果你全堆在App.vue里,写到后面基本没法维护。我按这个结构组织项目:

src/ api/ // 按模块封装的接口请求,如 elder.js、order.js views/ // 页面组件,如 elder/List.vue、order/List.vue router/ // 路由配置,动态路由和静态路由 store/ // 全局状态,只放用户信息和菜单 utils/ // axios封装、时间格式化等工具 layout/ // 后台整体布局

api/目录下的每个文件对应一个后端Controller。比如elder.js里封装了getElderList、addElder、updateElder、deleteElder四个函数,每个函数返回一个axios请求。好处是页面组件里不直接出现url,之后后端改地址只需要改一个文件。

6.2 表格和分页的联动,是CRUD页面的标准套路

社区养老系统里,老人列表页面算是“前台”,工单列表页面也跟它长得差不多。核心套路是:

页面初始化时执行fetchList(),参数是queryParams对象。表格的el-pagination组件触发current-change时更新queryParams.pageNum,再调用fetchList()。搜索按钮触发时,把pageNum重置为1,保证搜索后从第一页开始看。

const queryParams = { pageNum: 1, pageSize: 10, keyword: '' } async function fetchList() { const res = await getElderList(queryParams) if (res.code === 200) { tableData.value = res.data.list total.value = res.data.total } }

很多人写到这里会把整个tableData放到vuex里管理,这其实是个坏习惯。表格的临时数据刷新一次就变了,不存在跨页面共享的必要。vuex里真正该放的是用户信息和动态菜单,因为侧边栏需要全局读取,而页面刷新后vuex内容会丢失,所以还要在路由守卫里根据用户ID重新拉菜单。

6.3 新增和编辑复用同一个Dialog,少写一堆重复代码

老人档案的新增和编辑,弹窗内容几乎一样。很多同学会复制粘贴两个对话框组件,改半天还容易漏字段。正确做法是:一个el-dialog,用formTitle变量控制标题,提交时根据form.id是否存在判断是新增还是编辑。

保存成功后关掉弹窗,重新调用fetchList()刷新表格。这里有一个非常实用的细节:表单校验规则在校验失败时,this.$refs.form.validate()返回的Promise会reject,导致后续代码不执行。所以在then里写提交逻辑即可。

7. 部署与实测:启动后最容易翻车的三个坑

7.1 坑一:中文乱码,排查顺序很关键

数据库里的中文、页面传来的中文全部变成???,这个问题在前后端分离项目里尤其常见。排查顺序有三种:

  • 先看数据库连接串有没有characterEncoding=utf8;
  • 再看数据库表和字段的字符集是不是utf8mb4;
  • 最后看前端axios请求有没有设置Content-Type: application/json;charset=UTF-8(一般axios默认会带)。

这里要特别说明,utf8mb4和utf8在MySQL里不是一回事。utf8最多存储3字节,遇到生僻字或者Emoji会乱码,直接用utf8mb4最稳妥。

7.2 坑二:跨域问题,浏览器把请求拦了

前端跑在http://localhost:9528,后端跑在http://localhost:8080,端口不同就存在跨域。前端代理是一种方案,后端直接配置CORS更省事:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

CORS配置好以后,注册的拦截器要记得放行OPTIONS预检请求,否则前端发送复杂请求时会被拦截器当场拦截,报401错误。这个细节我在代码示例里已经体现:if ("OPTIONS".equals(request.getMethod())) return true;。

7.3 坑三:打包后页面刷新404

前端开发模式下一切正常,但npm run build之后把dist丢进Nginx或者和SpringBoot打成一起,一刷新页面就404。原因通常是前端路由用的是history模式。这种模式下,浏览器地址栏里的路由是假的(比如/system/user),后端没有对应的资源,自然404。

最省心的做法是直接用hash模式创建路由:const router = new VueRouter({ mode: 'hash', routes }),地址栏会变成/#/system/user。Hash模式下刷新页面不会请求服务器,也就不存在404问题。

如果你非要保留history模式,就需要在后端做路由重写,把无法匹配的路径转发到index.html,这个配置和你的部署方式强相关,对课程设计来说性价比不高。

7.4 验收清单:别等到答辩才发现功能缺一角

最后送上我跑这类系统的冒烟测试清单,照着过一遍基本就能安心了:

功能场景操作步骤预期结果
登录输入用户名密码登录成功,跳转首页并显示动态菜单
权限控制用服务人员账号登录不显示“用户管理”菜单
老人新增填写带身份证号的档案后端校验身份证格式,通过后列表出现新记录
老人搜索按姓名关键词搜索列表显示匹配结果,分页total正确
创建工单选择老人和服务类型生成待派单状态工单
派单管理员选择护工工单状态变为已派单,护工账号能看到该工单
服务完成护工填写反馈提交工单状态变为已完成
回访管理员填报满意度和回访内容工单状态变为已回访,无法再修改
健康记录添加一条老人血压数据趋势图出现新的数据点
账号停用管理员停用护工账号该护工无法登录系统

说实话,这类系统真正的难点不在新技术,而在状态和数据的一致性。我在实际带A同学跑这个项目的过程中,最深的体会是:先画清楚工单状态流转图,再动手建表,比什么都重要。工单走完了,档案和健康记录都只是另外两张基础表的问题。如果你正好拿到同学题的后续版本,比如还需要加志愿者管理、活动报名,这套表结构往上扩展也不吃力——核心的流程和数据模型不要推倒重做,后续加模块就是加表、加菜单、加页面的事。希望你做完之后,也能把每个字段为什么存在,讲得明明白白。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询