前后端分离的养老院管理系统,SpringBoot+Vue+MyBatis+MySQL这套技术栈,在目前的毕业设计和全栈实战项目里出现频率相当高。原因很简单:它把真实企业项目的开发方式完整地串了一遍——前端页面、后端接口、数据库设计、部署上线,一个循环下来,你对"项目怎么做出来"这件事就有了完整的体感,而不是停留在某个框架的 Hello World 阶段。
这次分享的这个养老院管理系统,定位很明确:面向养老院日常业务的信息化管理工具,覆盖老人档案、床位分配、护理记录、费用管理、家属沟通等核心环节。在没上系统之前,很多中小型养老机构还在用纸质台账和 Excel 表格,老人信息分散在各个护理员手里,床位状态要靠打电话问,费用结算月底对账对到头疼。这套系统就是要解决这些问题。
我会从项目整体的设计思路、后端核心实现、前端页面架构、完整部署流程、以及我在实际跑项目时踩过的坑这几个维度展开,尽量把每个关键技术点都讲透。如果你正准备拿这个项目做毕业设计,或者想通过一个完整项目来掌握前后端分离开发的节奏,这篇文章会非常对路。
1. 项目整体设计与技术栈选型
1.1 为什么一定要用前后端分离
养老院管理系统如果做成传统的 JSP 单体应用,也不是不行,但你会发现一个问题:业务一旦开始细化,前后端就互相绑架了。比如护理记录页面要加一个实时刷新的今日待办,后端得改页面模板;前端设计师调个样式,又得重新部署整个应用。前后端分离的核心价值,就是把"数据提供"和"页面展示"彻底拆开,两边各自独立开发、独立部署、独立扩展。
具体到这个项目里,后端只负责两件事:提供 RESTful API 接口、处理业务逻辑和数据库操作。前端只负责三件事:渲染页面、管理用户交互、通过 HTTP 请求和接口打交道。两边通过 JSON 格式的数据交换,互不干扰。这样做的好处非常实际:
- 开发并行:前端写页面的时候,后端可以先按接口文档提供 Mock 数据;后端调业务逻辑的时候,前端也不用来回等。项目周期能压缩三分之一以上。
- 部署灵活:后端可以扔在一台服务器上跑 8080 端口,前端构建出来的静态文件用 Nginx 托管,挂了前端不影响后端,后端升级也不影响页面访问。
- 扩展空间:以后如果要做移动端 App 或者小程序,后端接口可以直接复用,只需要重写前端壳子就行。
1.2 四个核心框架,为什么是它们
这套技术栈不是随便拼的,每一个组件都有明确的分工和选型理由。
SpringBoot:负责把后端工程"自动化"地搭起来。以前用 Spring 写一个 Web 项目,要配一堆 XML 文件,数据源、事务、扫描路径、视图解析器,每样都要手工声明。SpringBoot 用自动配置把这些琐事全部接管,你只需要在application.yml里写几行配置,项目就能跑起来。对于养老院管理系统这种典型 CRUD 业务为主的项目,SpringBoot 能让你把精力集中在业务逻辑上,而不是框架配置上。
Vue:前端部分用 Vue 的原因非常直白——学习曲线平缓、组件化开发效率高。系统里的老人档案表格、床位状态面板、费用统计图表,这些都是天然适合组件化的界面。Vue 的双向数据绑定特性,在表单页面的体验尤其舒服,比如新增老人档案时,输入框的校验、联动、清空,一套v-model就能解决,不需要像原生 JS 那样手动操作 DOM。
MyBatis:数据库访问层选 MyBatis 而不是 JPA/Hibernate,是因为这类管理系统的 SQL 往往有很强的业务针对性——多条件组合查询老人信息、统计各护理等级的人数、按月份汇总费用,这些操作用 MyBatis 的 XML 映射文件来写,一个清晰直观的 SQL 语句就搞定,相比 JPA 那种自动生成的查询,可控性要好得多。而且 MyBatis 的性能开销低,对 MySQL 的各种高级语法支持也好。
MySQL:数据存储是 MySQL 而不是更重的 Oracle 或 SQL Server,原因是这个系统的数据量级和管理员的运维水平决定的。养老院管理系统的数据量——几百位老人的档案、每天的护理记录、每月的费用流水——MySQL 完全可以轻松应对。安装简单、文档丰富、社区问题答案随手就能搜到,对非专职 DBA 的人来说足够友好。
这里顺便说一句,如果你照着这个项目跑的时候遇到 SpringBoot 版本问题,后面第 5 节我会专门讲版本匹配的坑。现在很多教程用的是 SpringBoot 2.x,但如果你自己新建项目默认拉到了 3.x,很多配置类会变,MyBatis 的 starter 依赖名也不同,别慌,那都是正常现象。
2. 后端核心设计:从数据模型到业务接口
2.1 数据库表结构:先想清楚业务流程
写代码之前,最先要做的是把数据模型设计好。养老院管理系统我拆成了这几个核心数据域:
- 老人档案域:核心表是
elder,字段包括姓名、性别、身份证号、出生日期、入住日期、健康状况、护理等级(自理/半自理/全护理)、紧急联系人电话、家属 ID。 - 床位管理域:
bed表记录床位号、房间号、楼层、床型(单人/双人)、当前状态(空闲/占住/维修)。老人和床位通过elder_bind关系表或直接在elder表里放bed_id字段关联。 - 护理记录域:
nursing_record表记录每次护理的详细信息:护理员 ID、老人 ID、护理项目(测血压、翻身、喂药等)、护理时间、备注。 - 费用管理域:
fee_record表记录费用产生时间、费用类型(床位费、护理费、餐饮费)、金额、应收应付状态。 - 员工与权限域:
staff表存员工信息,包括角色字段(管理员/护理员/财务/家属)。这里我用了一个简单的role字段来控制权限,没有单独做 RBAC 表,对于这个规模的项目已经够了。
实际设计的时候,我建议你在表之间多建几个合理的外键和索引。老人们最常按状态查询(比如"所有空余床位"),所以bed.status一定要建索引;费用记录经常按月统计,fee_record.create_time也要建索引。这个项目你要是拿去做毕设,数据库设计这一块一定要能讲出逻辑来,这是答辩时的重点加分点。
2.2 MyBatis 数据访问层:手写 SQL 的威力
MyBatis 在这个项目里承担的是"将 Java 方法和 SQL 语句绑定"的职责。我的习惯是在resources/mapper目录下放每个表的 XML 映射文件,比如ElderMapper.xml、BedMapper.xml,然后在对应的 Mapper 接口里声明方法。
举个例子,查老人列表时,页面上需要展示老人姓名、护理等级、所住床位号、家属联系电话。这些数据分布在elder、bed、staff三张表里,用 MyBatis 写一个自定义查询:
<select id="selectElderList" resultType="map"> SELECT e.id, e.name, e.nurse_level, e.health_status, b.bed_no, b.room_no, s.phone AS family_phone FROM elder e LEFT JOIN bed b ON e.bed_id = b.id LEFT JOIN staff s ON e.family_id = s.id <where> <if test="name != null and name != ''"> AND e.name LIKE CONCAT('%', #{name}, '%') </if> <if test="nurseLevel != null"> AND e.nurse_level = #{nurseLevel} </if> </where> ORDER BY e.create_time DESC </select>这种多条件动态拼 SQL 的能力是 MyBatis 的看家本领。<where>标签会自动处理 AND 的拼接,<if>标签实现条件判断——有没有这个查询条件都能执行,非常适合列表页的筛选功能。
关于 MyBatis 缓存,也简单说两句。MyBatis 有一级缓存和二级缓存。一级缓存是 SqlSession 级别的,同一个会话里执行相同的 SQL,直接从缓存返回,这个默认开启。二级缓存是 mapper 级别的,需要手动配置,跨会话也能命中。但在这个项目里,我建议二级缓存慎用。因为养老院管理系统的数据变更很频繁,老人状态、床位状态随时在变,如果用了二级缓存,可能你刚改完数据,页面读到的还是旧状态。我在第 5 节会讲一个我因为这个缓存机制排查了半天的实际问题。
2.3 接口设计与统一返回格式
后端接口的设计直接影响前端开发的效率。这个项目里,所有接口都遵循 RESTful 风格,按资源命名:
GET /api/elder/page—— 分页查询老人列表(带筛选条件)POST /api/elder—— 新增老人档案PUT /api/elder/{id}—— 修改老人信息(例如护理等级变更)PUT /api/bed/assign—— 分配床位POST /api/nursing/record—— 录入护理记录GET /api/fee/statistics—— 月度费用统计
这里有一个关键实践:所有接口返回统一的数据结构。我在项目里定义了一个Result类:
public class Result<T> { private Integer code; // 200 成功,500 失败 private String message; // 提示信息 private T data; // 响应数据 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.code = 500; result.message = message; return result; } }这样做的好处,前端只需要统一处理code字段,为 200 就渲染数据,否则弹出message里的错误提示。不用每个接口都去解析不同的数据格式,省掉大量无意义的联调时间。
另外要提到认证方案。这个项目用的是 JWT(JSON Web Token)。用户登录后,后端校验账号密码,生成一个带有用户 ID 和角色的 token 返回前端。前端在后续请求的请求头里带上Authorization: Bearer <token>,后端通过拦截器验证 token 合法性。我在WebMvcConfig里注册了一个自定义的AuthInterceptor,专门处理无需鉴权的白名单(比如登录接口),其他接口全部拦截。这一块的代码量不大,但它是系统安全性的底线,不能让任何人随便调接口改数据。
3. 前端架构与核心页面实现
3.1 Vue 项目结构与环境搭建
前端工程是基于 Vue CLI(如果你用 Vite 也可以,后面我会说区别)创建的标准项目结构。目录安排是这个项目的骨架:
src/ ├── api/ // 接口请求封装,按模块拆分 │ ├── elder.js │ ├── bed.js │ └── fee.js ├── assets/ // 静态资源,图片、全局样式 ├── components/ // 通用组件 │ ├── Pagination.vue │ └── StatusTag.vue ├── router/ // 路由配置 ├── store/ // Vuex 状态管理 ├── views/ // 页面组件 │ ├── Login.vue │ ├── Dashboard.vue │ ├── elder/ │ │ ├── ElderList.vue │ │ ├── ElderForm.vue │ ├── bed/ │ │ └── BedManage.vue │ └── fee/ │ └── FeeManage.vue └── main.js关于 Vue 环境配置,我在项目里最常踩的坑是新装 Node.js 之后,npm install报错。这里给一个经验之谈:用 Node 16 或 18 的 LTS 版本,不要追新。Vue CLI 5 和某些依赖在 Node 20+ 上面会出现兼容性问题。你要是用 Vite 创建 Vue 3 项目,Node 版本要求会宽松一些,但如果是照着我的这个项目结构来,用 Node 16 的 LTS 版本最稳。
3.2 几个核心页面的实现思路
登录页:虽然看起来简单,但它是前端里第一个体现"前后端配合"的页面。用户输入账号密码后,调POST /api/login接口,拿到 token,存放在localStorage并同步到 Vuex,然后路由守卫根据 token 判断是否跳转首页。同时在请求拦截器里给 Axios 实例加上请求头:
axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = 'Bearer ' + token; } return config; }, error => Promise.reject(error));老人档案管理页:这是整个系统最核心的页面,采用了"表格 + 搜索表单 + 弹窗表单"的组合。表格用 Element UI 的el-table,搜索区是护理等级、状态几个筛选项,点击查询按钮后重新请求分页数据。这个页面充分体现了 Vue 组件的优势——el-table、el-dialog、el-form这些都是成熟的现成组件,把开发精力留给业务逻辑。
床位管理页:我的做法是一个可视化的房间床位面板。每个房间是一个卡片,房间内的床位用不同颜色标识——绿色空闲、红色占用、灰色维修。点击空闲床位,弹出分配窗口,选择老人,调用床位分配接口。这个页面用到了 Vue 的列表渲染v-for和动态绑定 class:class,它是整个前端代码里最直观展示 Vue 响应式原理的地方——床位状态一变,页面自动刷新,不需要手动任何 DOM 操作。
费用管理页:表格还原始费用明细,顶部用el-date-picker选择统计月份,调月度统计接口,用el-card展示总费用统计卡。这里的前端代码并不复杂,关键在于和 MyBatis 的日期函数配合好,后面第 5 节我会讲一个时间范围查询的坑。
3.3 跨域问题与开发代理
前后端分离开发时,最烦也最常遇到的第一个问题就是跨域。前端跑在http://localhost:8080,后端在http://localhost:9090,浏览器会因为同源策略拦截跨端口的请求。
开发环境的解决办法有推荐,就是在 Vue 项目的vue.config.js里配置代理:
module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } } };这样前端请求/api/elder/page时,开发服务器会把它转发给后端的9090端口,浏览器角度来看是"同源"的,没有跨域问题。而后端不需要做任何 CORS 配置,因为生产环境你直接用 Nginx 反代,同样把/api转发到后端服务。
4. 完整部署流程:从零到能跑起来
4.1 MySQL 安装与数据库初始化
这一节我会按 Windows 环境来写(如果你用 Linux 服务器部署,步骤大同小异)。MySQL 我建议装 5.7 或 8.0 版本,不要学我一开始用了最新版。最新的 MySQL 8.4 在某些认证插件上和老项目的 JDBC 驱动不兼容,容易报错。
安装的要点就两个:记住你的 root 密码、选择正确的字符集。字符集一定要选 UTF-8,或者安装后执行:
SET NAMES utf8mb4;如果不设置,默认的 latin1 会导致中文乱码,整个项目跑起来全是一排问号,非常崩溃。
初始化数据库的时候,我用项目的sql/init.sql文件来完成建库建表。命令行执行:
mysql -u root -p < init.sql或者用可视化工具(Navicat、SQLyog)直接导入。init.sql里包含CREATE DATABASE、所有表的建表语句,以及一批测试数据。导入后,在项目的application.yml里配置数据库连接:
spring: datasource: url: jdbc:mysql://localhost:3306/elder_care?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver这里注意serverTimezone=Asia/Shanghai必加,否则 Java 8 以上的 JDBC 驱动和 MySQL 的时区不一致,会报时间相关的错误。这个问题在 Windows 上尤其常见。
4.2 SpringBoot 后端打包与运行
后端项目用 Maven 管理依赖,运行前先确认pom.xml里的依赖是完整的。核心依赖就四个:spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、以及 JWT 的jjwt。打包命令:
mvn clean package -DskipTests这个命令会在target目录下生成一个xxx.jar文件。要在服务器上运行,直接用:
java -jar target/elder-care-system.jar默认端口我配置成了 9090(在application.yml里server.port设置),因为 8080 要给前端开发服务器或者 Nginx 用,避免抢端口。
如果你想在 Windows 上后台运行,可以写一个.bat脚本:
@echo off start javaw -jar elder-care-system.jar如果是 Linux 服务器,推荐用nohup或者直接配置成 systemd 服务。后续杀进程的问题,Windows 上就用netstat -ano | findstr :9090找出 PID 然后taskkill,Linux 上lsof -i :9090配合kill -9。
4.3 Vue 前端构建与 Nginx 发布
前端开发完成后,构建生产包:
npm run build构建成功后会在dist目录下生成静态文件。这些文件直接扔给 Nginx 就行。Nginx 配置的关键在于搞定前端路由的 history 模式和 API 反向代理。
Vue 路由如果用了history模式(去掉 URL 里的#),刷新页面时 Nginx 需要把try_files重定向到index.html,否则刷新页面就是 404。这是前端部署最常见的坑之一。
完整的 Nginx 配置参考:
server { listen 80; server_name your_domain_or_ip; # 前端静态资源 location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端接口转发 location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }配置完成后nginx -s reload。到这里,整个系统就正式上线了,你通过浏览器访问服务器 IP 就能看到登录页。
5. 常见问题排查与避坑实录
5.1 前端一列中文变问号
这个问题的根源不复杂:MySQL 表或连接字符集不对。排查链路是——先看application.yml里的连接串characterEncoding=utf8是否配置,再看 MySQL 表本身字符集。我的解决办法是建库时强制执行 UTF-8:
CREATE DATABASE elder_care DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;如果你在 Windows 里操作 MySQL,还有一个隐藏坑:MySQL 安装时如果选了默认的utf8(其实是 utf8mb3),部分生僻汉字和 emoji 字符仍然存不进去。保险起见就是表级和库级都指定 utf8mb4。
5.2 MyBatis 查询结果字段全是 null
这是新手最容易懵的问题。如果elder表里的字段是下划线命名(比如nurse_level),但实体类属性是驼峰命名(nurseLevel),MyBatis 默认不会自动映射。
解决办法有两种,推荐直接在配置里开启驼峰映射:
mybatis: configuration: map-underscore-to-camel-case: true这个配置让 MyBatis 自动把数据库的nurse_level映射到 Java 的nurseLevel,不需要手写每一个resultMap。
5.3 修改数据后,接口读到的还是旧数据
这大概率是 MyBatis 二级缓存的问题。我之前在某个 mapper 上开启了二级缓存,后来发现一个诡异现象:管理员在后台修改了老人护理等级,但页面查询接口返回的还是旧数据。排查了很久才意识到是缓存没失效。
定位方法很简单:看 MyBatis 的 SQL 日志(logging.level.com.example.mapper=debug开启),发现同样的 SQL 没有真正执行,而是命中缓存。对于管理类系统,我建议干脆关掉二级缓存,靠 MySQL 的查询性能已经足够,不要为了那点性能提升引入数据一致性问题。
5.4 Vue 项目npm install中途报错或卡死
这个地方踩过的坑最多。新拉下来的 Vue 项目在npm install时经常遇到网络问题或依赖冲突。
我的经验排序:
- 优先换 npm 镜像源(比如用淘宝镜像,注意这是常规操作里的正常做法)。
- 删除
node_modules和package-lock.json,重新npm install。 - 检查 Node 版本是否和依赖兼容。
另外强烈建议用nvm管理 Node 版本,而不是把 Node 直接装死在系统里。这样每次遇到项目依赖版本问题,切一个 Node 版本再试,几乎都能解决。
5.5 后端接口返回 401 或一直跳登录
这个问题多半出在 JWT 拦截器的路径配置上。我在AuthInterceptor里配置了需要放行的路径,但很容易少写了某个静态资源路径或某个接口的前缀。排错思路是:先看后端控制台的拦截日志,确定是拦截器拦截还是 token 校验失败;再看前端 Axios 是不是真的在请求头里带了 token。
这里给一个判断技巧——打开浏览器开发者工具的 Network 面板,看请求头里有没有Authorization。如果没有,问题在前端拦截器配置;如果有且后端还报 401,问题在后端 token 校验逻辑。
5.6 SpringBoot 版本过高导致的兼容问题
开头提了 SpringBoot 版本问题,这里展开说。如果你直接用 Spring Initializr 创建项目,默认拉的是 SpringBoot 3.x,但很多老教程的依赖写法都是 2.x 的。最直接的区别:
<!-- SpringBoot 2.x --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <!-- SpringBoot 3.x --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency>SpringBoot 3.x 基于 JDK 17+,如果你的本地 JDK 还停留在 8,那就选 SpringBoot 2.7.x 系列。我的建议:不是版本越新越好,能让项目稳定跑起来的版本才是好版本。部署教程里没有强调这一点,但实际拿源码跑的时候,八成的人卡在第一关就在这里。
5.7 费用按月统计查不出数据
这个问题的坑在时区。统计某月的费用,前端传的是"2025-03",后端如果用WHERE create_time = '2025-03'这种写法,通常得不出正确结果。正确写法是用 MySQL 的日期区间函数:
<select id="selectFeeStatistics" resultType="map"> SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, SUM(amount) AS total_amount FROM fee_record WHERE create_time >= #{startDate} AND create_time < #{endDate} GROUP BY month </select>startDate和endDate是前端的月份参数处理过的边界值,比如查 3 月就传2025-03-01和2025-04-01(开区间)。这样既避免了DATE_FORMAT导致的索引失效问题,还能兼容跨年数据。
我个人在实际操作中的体会是:这种管理系统项目的复杂度不在某个单独的技术难点,而在把一套完整业务流程从头到尾串起来的工程能力。数据库设计、后端接口、前端页面、部署发布,每个环节看着都简单,但联动起来出问题的地方恰恰是跨模块的衔接处——时区格式、字段命名风格、端口冲突、跨域配置、缓存策略。
最后再分享一个对做这类项目非常实用的小技巧:给所有接口加上日志输出。在 SpringBoot 的配置里打开 MyBatis 日志,再把业务层的关键操作(比如新增、删除、改状态)用@Slf4j的log.info打印出来。遇到问题的时候,看日志比猜问题快十倍。这个系统本身是一个很典型的前后端分离实战项目,从代码结构到部署流程完全贴合真实企业项目的开发范式,建议你拿到源码后,自己动手把"部署文档里的每一步"完整执行一遍——能跑通整套流程,你对这套技术栈的信心会完全不一样。