如果你正在找一套能直接拿来改、能跑通、能写进简历或毕业设计的全栈管理系统源码,SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 这套养老院管理系统,恰好就是典型的“前后端分离 + 权限管理 + CRUD 业务闭环”的项目形态。这套组合这两年几乎是 Java Web 领域出镜率最高的搭配,从毕设选题到企业内部实训项目都在用它。我的看法是,与其东拼西凑找各种零散的教程,不如直接拿到一套结构完整的源码,再读懂它的设计逻辑,在此基础上做二次开发和功能扩展,反而是最快的学习路径。
这篇文章我会围绕这套养老院管理系统,从整体模块拆分、技术选型逻辑、核心功能实现、常见问题排查这几个角度展开,把我自己动手拆解和重构这类项目时的经验一并写出来。不管你是刚学完 SSM 想进阶前后端分离,还是准备交毕业设计,或者工作中需要快速搭一套后台管理框架,这篇文章的内容应该都能帮你省下不少时间。
1. 项目全貌与设计思路
1.1 养老院管理系统到底要管什么
养老院管理系统这类业务系统,表面上看起来就是一堆增删改查,但真正拆开需求之后你会发现,它的业务实体之间关联性很强,恰好适合拿来练手完整的前后端交互逻辑。一个典型的养老院管理系统,核心要管的业务实体大概有这几类:
- 老人信息管理:姓名、性别、年龄、身份证号、入住时间、房间号、床位号、家属联系方式、健康状态、护理等级。
- 护工管理:护工编号、姓名、手机号、负责区域、值班安排、工作记录。
- 床位与房间管理:房间号、床位号、床位状态(空置/已入住)、房间类型(单人间/双人间/多人间)、楼层。
- 费用管理:床位费、护理费、餐饮费、医疗费,按月生成账单,记录缴费状态。
- 健康档案:体检记录、用药记录、血压血糖数据、过敏史。
- 访客登记:来访人信息、与被访老人关系、来访时间、离开时间。
- 系统管理:用户账号、角色权限、菜单管理、操作日志。
这些模块之间有着清晰的从属关系,比如老人必须关联床位,而床位必须属于某个房间;费用账单必须关联到某个老人。这种关联关系在数据库表设计阶段就要梳理清楚,不然写代码的时候会频繁遇到联表查询或数据不一致的问题。
从角色划分的角度来看,系统至少需要三种角色:系统管理员负责全局配置和账号管理,护工人员负责日常护理记录的录入和维护,老人家属可能需要一个只读的登录入口去查看老人的健康档案和费用情况。当然,如果你只是做毕设,也可以把家属端省掉,只保留管理员和护工两个角色,但我的建议是做一个轻量的家属端登录,哪怕只是查看信息,也能在答辩或者简历上多点亮点。
1.2 技术选型背后的逻辑
这套系统的技术栈是 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,这个组合不是随便拼起来的,每一层都有很现实的选择理由。
先看后端。SpringBoot2 是目前 Java Web 领域最稳妥的版本,社区生态极其成熟,各种坑几乎都被踩平了,你搜一个问题基本都能找到答案。SpringBoot3 虽然已经出来一段时间了,但它基于 JDK17 和 Jakarta EE,很多老项目依赖和教程都还没完全迁移过去,对于学习项目和老版本中间件兼容来说,SpringBoot2 依然是更稳的选择。如果你以后打算进企业做维护,SpringBoot2 的存量项目也远多于 SpringBoot3。
和 SpringBoot2 搭配的持久层框架,MyBatis-Plus 是当前效率最高、代码量最少的选择。传统 MyBatis 需要手写大量 XML 映射文件,而 MyBatis-Plus 在继承 BaseMapper 之后,单表的 CRUD 几乎不用写 SQL,条件构造器 QueryWrapper 也能覆盖绝大多数查询场景。它提供的分页插件、逻辑删除、字段自动填充、乐观锁插件,正好都是管理系统里高频用到的能力。对比 JPA 来说,MyBatis-Plus 保留了 SQL 的可控性,复杂的多表查询你可以自己写 SQL,不像 JPA 那样遇到复杂查询就头大。
前端选 Vue3 而不是 Vue2,核心原因是 Composition API 和 setup 语法糖带来的代码组织能力提升。管理系统通常包含大量列表页、表单页、弹窗交互,如果用 Vue2 的 Options API,一个稍微复杂的页面动辄几百行代码,逻辑散落在 data、methods、watch 里,维护起来相当吃力。Vue3 的 Composition API 允许你按功能模块组织代码,比如用户列表页可以把请求数据的逻辑、搜索逻辑、分页逻辑分别封装,可读性和复用性都上了一个台阶。同时 Vite 带来的开发体验提升也很明显,冷启动基本是秒开。
MySQL8.0 相比 5.7,带来了窗口函数、公用表表达式(CTE)、更好的 JSON 支持、utf8mb4 默认字符集、以及更完善的索引优化器。对这套养老院系统来说,MySQL8.0 的窗口函数可以很优雅地解决一些排名、环比计算的问题,CTE 也能让复杂的多表统计查询变得更加清晰。而且现在新安装的 MySQL 基本都是 8.0,教程和运维资料也都在往新版迁移,没有必要再用 5.7 去折腾兼容性。
建议:如果你需要把 MySQL8.0 部署在本地开发环境,直接用 Docker 是最高效的方式,一条命令就能跑起来,不用处理 Windows 和 Linux 上的安装差异。后面我会具体说一条实测可用的命令。
2. 核心技术点拆解与实操要点
2.1 SpringBoot2 后端骨架搭建要点
拿到一套源码之后,别急着运行,先看它的包结构。一个合理的 SpringBoot2 后端工程,通常会按职责分层,而不是按功能模块堆文件。以这套养老院系统为例,推荐的包结构是这样的:
com.example.eldercare ├── controller // 接口层,接收请求、返回结果 ├── service // 业务层,处理核心逻辑 │ └── impl // 业务实现类 ├── mapper // 数据访问层,MyBatis-Plus 的 Mapper 接口 ├── entity // 数据库实体对象 ├── dto // 请求参数对象 ├── vo // 返回视图对象 ├── config // 配置类,如 MyBatis-Plus 分页插件、跨域配置 ├── common // 通用类:统一返回结果、全局异常处理、常量 ├── utils // 工具类:JWT工具、日期工具等 └── interceptor // 拦截器:登录认证、日志记录这套结构的核心思想是单向依赖:Controller 只调 Service,Service 只调 Mapper,实体类不依赖任何层。如果你发现某个 Controller 里直接注入了一个 Mapper,那这段代码就是不合格的,因为业务逻辑泄漏到了接口层,后续要加缓存或者改逻辑的时候会非常痛苦。
统一返回结果封装是后端工程必做的一件事。我见过太多接口返回格式不一致的问题,有的成功返回 data 字段,有的返回 rows 字段,前端对接的人得为每个接口单独写适配逻辑。正确做法是定义一个 Result 类,包含 code、message、data 三个字段,所有接口无论成功失败都返回这个结构。比如:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); 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 注解拦截所有 Controller 层抛出的异常。业务方法里遇到参数不合法、数据不存在之类的情况,直接抛自定义的 BusinessException,由全局处理器统一捕获并转成 Result 返回。这样 Controller 里的代码可以写得很干净,不用到处 try-catch。
后端的登录认证是管理系统里躲不开的环节,这套项目在实现时选的是 JWT 方案。流程是:用户提交用户名密码,后端校验通过后签发一个带过期时间的 token,前端拿到 token 存在本地,之后每次请求带上 Authorization 请求头,后端通过拦截器解析 token 判断用户身份。JWT 方案相比传统的 Session 方案,最大的好处是后端无状态,不依赖内存存储,水平扩展时不需要考虑 Session 同步问题。当然,JWT 也有一个要注意的点,就是 token 一旦签发,在过期之前是无法主动废除的,如果用户改了密码或被管理员禁用账号,旧 token 在过期前依然有效。如果项目对安全性要求高,可以考虑引入 Redis 做 token 黑名单,但学习项目里做到 JWT 解析鉴权这一步已经够用。
2.2 MyBatis-Plus 的高效玩法
MyBatis-Plus 在日常 CRUD 开发里能帮你省掉至少一半的代码量,但这些便利背后有几个关键点必须吃透,否则会踩到效率反而不如手写 SQL 的坑。
最核心的用法是继承 BaseMapper。你的 Mapper 接口只要写成这样:
public interface ElderMapper extends BaseMapper<Elder> { // 单表 CRUD 不需要写任何方法 }就自动拥有selectById、selectList、insert、updateById、deleteById这些方法。配合 ServiceImpl,Service 层的实现也变得非常薄,大部分情况下只需要调用几下父类方法即可完成业务。比如老人入驻时要做几件事:更新床位状态、生成一条入住记录、计算当月费用。这个逻辑写在 Service 层,使用 @Transactional 事务注解包起来,任何一步失败都会整体回滚,保证数据一致性。
条件构造器是 MyBatis-Plus 最值得花时间掌握的部分。日常的按条件查询,比如“查所有 80 岁以上的男性老人,按入住时间倒序”,用 LambdaQueryWrapper 一行就能表达:
LambdaQueryWrapper<Elder> wrapper = new LambdaQueryWrapper<>(); wrapper.ge(Elder::getAge, 80) .eq(Elder::getGender, "男") .orderByDesc(Elder::getCheckInTime); List<Elder> elders = elderMapper.selectList(wrapper);用 Lambda 表达式引用实体字段,好处是编译期就能检查字段名是否正确,不会出现因为字符串写错导致运行期才报 SQL 错误的情况。这个特性在重构时尤其有用,你改了实体字段名,如果用的是 LambdaQueryWrapper,IDE 会帮你找出所有引用点,而用字符串写法的 QueryWrapper 只能靠测试去发现。
分页功能是管理系统列表页的刚需。MyBatis-Plus 分页需要两步:先注册分页插件,再调用分页方法。注册插件的方式是在配置类里加上:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }分页插件的作用是拦截你执行的 SQL,自动拼接 LIMIT 语句,并执行一次 COUNT 查询。如果你忘了注册这个插件,selectPage方法返回的数据里 total 字段会一直是 0,而且查出来的其实是全量数据,页面上的分页功能自然就失效了。这是新手最容易踩的坑,后面我会专门说到。
逻辑删除功能在管理系统里也很实用。比如删除一条老人入住记录,如果物理删掉,后续审计就查不到历史了。MyBatis-Plus 的 @TableLogic 注解可以让 delete 操作变成 update 操作,把一个自定义的 deleted 字段置为 1,查询时自动过滤已删除的数据。这个机制的核心价值,是让你既能做“软删除”,又不用在每个查询 SQL 里手写WHERE deleted = 0。但要注意:逻辑删除会让一些依赖物理行数统计的场景变得复杂,比如统计数据总数时如果没过滤 deleted 字段,结果就会偏大。
字段自动填充也是我强烈推荐使用的一个特性。数据库表里一般都会有create_time和update_time字段,以前每次插入或更新都要手动 set,写多了就烦。通过 @TableField(fill = FieldFill.INSERT) 注解配合自定义的 MetaObjectHandler 处理器,MyBatis-Plus 会在执行 insert 和 update 时自动填入当前时间,你的业务代码里完全不用再处理这两个字段。
2.3 Vue3 前端核心实现思路
Vue3 项目如果是从零搭,现在的主流方式是使用 Vite 官方脚手架。和 Vue2 时代常用的 Vue CLI 相比,Vite 最大的优势是开发服务器启动极快,而且修改代码后的热更新几乎是即时生效的,不需要等待 Webpack 重新编译整个项目。团队开发时,这种开发体验的差距会直接体现在效率上。
前端的工程化结构建议和业务页面分开整理。如果把所有组件都堆在 views 文件夹里,页面一多就乱成一锅粥。合理的做法是:src/api 目录下按业务模块拆封接口请求文件,src/views 目录下按页面拆组件,src/components 目录下放通用组件,src/router 放路由配置,src/utils 放请求封装和工具函数。
axios 封装是前端项目里必须做的一步。不做封装的直接后果是:每个页面请求都要重复写请求头、处理错误状态码、拼接 token,代码冗余而且容易出 bug。推荐的封装逻辑是创建一个 axios 实例,设置 baseURL 和超时时间,用请求拦截器统一注入 token,用响应拦截器统一处理业务错误码和 HTTP 状态码。当后端返回 401 时,前端自动跳转到登录页并清空本地存储的用户信息,这个逻辑写在响应拦截器里一次到位,不用每个页面重复写。
路由守卫配合后端角色权限做页面访问控制,也是这套系统的关键环节。Vue Router 提供了全局前置守卫beforeEach,每次路由跳转前先检查本地有没有 token,没有就跳到登录页;有 token 再检查路由配置里的 meta 字段是否包含当前用户角色,不包含就跳到 403 页面。这套逻辑可以保证,哪怕用户手动修改了前端的路由地址,也无法访问没有权限的页面。
拿养老院系统的老人列表页举例,一个完整的前端页面大致包含三个部分:搜索表单区、数据表格区、弹窗表单区。搜索表单区包含姓名、年龄范围、床位状态等筛选条件;数据表格区使用分页组件展示老人信息,行内提供编辑、详情、删除按钮;弹窗表单区用于新增和编辑老人信息,表单校验规则用 Element Plus 的属性配置实现。页面的 Composition API 逻辑可以拆成四段:响应式数据定义、页面初始化数据请求、搜索/重置逻辑、新增/编辑/删除操作逻辑,每段都封装成函数,代码清清爽爽。
3. 实操过程与核心环节实现
3.1 环境准备与项目初始化
动手跑这套系统之前,先把环境准备好。我给出一份实测可行的清单:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 8 或 11 | SpringBoot2.7.x 在 JDK8 和 11 下运行稳定 |
| Maven | 3.6.3 以上 | 建议使用阿里云镜像加速依赖下载 |
| Node.js | 16.14.x 或 18.x | Vite3/Vite4 对 Node 版本有最低要求 |
| MySQL | 8.0.x | 注意时区设置 |
| IDE | IDEA 2023+ | 建议安装 Lombok 插件 |
| 前端包管理 | npm 或 pnpm | pnpm 安装依赖更快 |
MySQL8.0 的安装,Windows 用户可以直接用安装包,Mac 和 Linux 用户我建议直接上 Docker。这里推荐一条我实测过很多次的命令,开箱即用:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123456 \ -e TZ=Asia/Shanghai \ -v mysql_data:/var/lib/mysql \ mysql:8.0这段命令里有几个细节值得注意:TZ=Asia/Shanghai环境变量解决了容器默认时区是 UTC 的问题,避免插入数据时的日期时间比北京时间少 8 小时;-v mysql_data:/var/lib/mysql是数据卷持久化,容器删了数据还在,不会因为 docker rm 而丢失数据库内容。使用 Docker 部署 MySQL8.0 的好处是环境干净,卸载也方便,但要注意防火墙不能占用 3306 端口,否则容器启动成功数据库连不上的情况又会出现。
数据库连接串的配置也需要重点标记。MySQL8.0 和 5.7 在 JDBC 驱动和连接参数上有差异,最明显的体现是驱动类从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver,而且新版驱动默认不会帮你处理时区,所以连接串里必须显式声明 serverTimezone。保险的配置是这样的:
spring: datasource: url: jdbc:mysql://localhost:3306/eldercare?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: root123456 driver-class-name: com.mysql.cj.jdbc.DriveruseSSL=false是因为本地开发环境不需要 SSL 加密连接,不加这个参数新版驱动会打印大量告警日志。allowPublicKeyRetrieval=true则是因为 MySQL8.0 的 caching_sha2_password 认证插件在非 SSL 连接下,需要客户端主动获取服务端公钥才能完成身份认证。
3.2 核心模块实现示例:老人档案管理
一套管理系统拿来之后,读懂一个核心模块的完整链路,其他模块基本就能触类旁通了。我拿老人档案管理来做拆解,因为它是整个系统的数据基础,其他模块都围绕它展开。
先看数据库表设计。老人基本信息表的建表 SQL 关键片段如下:
CREATE TABLE elder ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT NOT NULL COMMENT '性别 1男 2女', age INT NOT NULL COMMENT '年龄', id_card VARCHAR(18) NOT NULL COMMENT '身份证号', phone VARCHAR(20) COMMENT '老人本人电话', bed_id BIGINT COMMENT '床位ID', room_id BIGINT COMMENT '房间ID', care_level TINYINT COMMENT '护理等级 1自理 2半自理 3全护理', health_status VARCHAR(200) COMMENT '健康状况描述', check_in_time DATETIME COMMENT '入住时间', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除标记', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='老人信息表';这里面有两个设计细节想重点说一下。
第一个是逻辑删除字段。我在前面提到过 @TableLogic 的逻辑删除方案,数据库层面这个字段默认值是 0,删除操作执行时实际上是把 deleted 从 0 改成 1。字段设计上最好给默认值,并且索引要考虑到查询条件里带上 deleted=0 的情况,避免全表扫描。
第二个是 bed_id 和 care_level 的设计。床位和老人是一对一关系,老人入住时检查床位状态是否为空,入住后把床位状态改为已住。护理等级这种枚举值,数据库里存 TINYINT 数字,前端通过字典映射显示文字,比如 1 显示为“自理”、2 显示为“半自理”、3 显示为“全护理”。这种做法的好处是灵活,新增一个护理等级只需要改前端字典,不用改数据库结构,也不用改后端逻辑。
实体类写好后,Mapper 接口一行就能继承 CRUD 能力。Service 层的核心逻辑集中在“入住办理”这个业务动作上。我当时重构这段逻辑时,把它的处理步骤总结成了四步:
- 查询床位状态,如果已被占用则抛出业务异常;
- 插入老人基本信息;
- 更新床位状态为已入住;
- 生成第一条费用记录,默认以当前月第一天为计费开始时间。
这四个步骤必须在一个事务里执行。如果不加事务,前两步执行成功后第三步报错,数据就出现了“老人存在但床位没绑定”的脏数据。用 @Transactional 注解就可以避免这种局面。
Controller 层的接口设计走 RESTful 风格,列表接口用 GET 请求加分页参数,新增用 POST,修改用 PUT,删除用 DELETE。参数用 DTO 对象接收,通过 JSR303 的 @Validated 注解做基础校验,服务端校验是安全底线,前端校验只是优化体验。
前端的新增老人页面,用的是 Element Plus 的 el-form 组件。表单校验规则配置在 rules 对象里,比如姓名必填、身份证号格式匹配 18 位、年龄范围在 0 到 120 之间。提交时先执行 validate 方法,通过后才调用新增接口。整个链路的调用顺序是:el-form 提交事件 → api 模块的 addElder 函数 → axios PUT 请求 → 后端 Controller 接收参数 → Service 处理业务 → Mapper 执行 SQL → 返回 Result 对象 → 前端弹窗提示成功。
分页列表的实现是另一个重点。前端请求参数包含 pageNum、pageSize、name、gender、careLevel 等字段,后端用 Page 对象接收,MyBatis-Plus 自动分页,最后返回一个包含 total 和 records 的分页结果对象。前端表格用 el-pagination 组件,current-page 和 page-size 分别绑定到对应的响应式变量,change 事件触发时重新请求数据。这套流程在管理系统里几乎是标准模板,一个项目里会重复出现几十次,值得一次吃透,后面就是复制改参数的事。
3.3 权限体系与登录认证实现
养老院系统的权限模型,基于 RBAC(基于角色的访问控制)设计,涉及五张核心表:用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。用户只能通过绑定角色来获得权限,角色通过菜单表配置来决定能看到哪些页面、操作哪些按钮。
后端登录接口的逻辑,我梳理一下关键步骤:
public LoginVO login(LoginDTO dto) { // 1. 调用用户服务查询用户 User user = userService.getByUsername(dto.getUsername()); if (user == null) { throw new BusinessException("用户名或密码错误"); } // 2. 密码加盐比对 if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) { throw new BusinessException("用户名或密码错误"); } // 3. 查询用户角色和权限标识 List<String> roles = userService.getRolesByUserId(user.getId()); List<String> perms = userService.getPermsByUserId(user.getId()); // 4. 生成 JWT token String token = jwtUtils.generateToken(user.getId(), user.getUsername(), roles); // 5. 组装登录结果 LoginVO vo = new LoginVO(); vo.setToken(token); vo.setUserInfo(user); vo.setRoles(roles); vo.setPerms(perms); return vo; }密码加密使用 BCrypt 算法而不是 MD5。原因很简单,MD5 是哈希算法,彩虹表攻击可以轻松反查弱密码,而 BCrypt 内部会自动加盐,同一密码每次加密出来的密文都不一样,安全性高出好几个量级。这个知识点在技术面试里经常被问到,值得花时间深入理解。
数据库里的用户信息表,除了基础账号字段,还应该包含状态字段,用来标记账号是否被禁用。登录时如果状态为禁用,直接拦截,防止被踢出系统的用户在 JWT 有效期内继续访问接口。
前端路由守卫的实现,核心思路是在beforeEach里检查有没有 token,同时检查路由 meta 里配置的角色是否与当前用户匹配。代码示例如下:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token) { if (to.path === '/login') { next() } else { next('/login') } return } // 根据后端返回的角色判断是否有权访问 const requiredRoles = to.meta.roles if (requiredRoles && !requiredRoles.includes(store.state.user.role)) { next('/403') return } next() })还有一个细节:退出登录时除了清除本地 token,最好调用后端接口记录操作日志。以后审计翻记录时,这一步能帮你省掉不少排查纠纷的时间。
按钮级权限控制,使用 Vue3 的自定义指令或权限函数实现。比如“删除老人档案”这个按钮,只对管理员角色可见,前端通过 v-permission 指令做判断。需要注意的是,按钮级权限只是在体验层面做了隐藏,真正的安全边界必须由后端接口的权限校验来兜底,前端隐藏按钮并不能防止有人直接调接口。
4. 常见问题与排查技巧实录
4.1 新手最常踩的坑
这套技术栈在国内用得极其广泛,网上相关的资料也很多,但恰恰因为人多,踩坑的方式也五花八门。我盘点几个这个项目里出现频率最高的问题,附带排查思路,你可以把这些整理成自己的速查表。
问题一:MySQL8.0 连接报错
启动后端项目时,控制台出现Public Key Retrieval is not allowed或者Unable to load authentication plugin 'caching_sha2_password',核心原因就是连接串没带 allowPublicKeyRetrieval=true 参数,或者驱动版本不是 8.x 的 mysql-connector-java。解决方法是确保 pom.xml 里驱动依赖版本是 8.0.x,同时在 JDBC 连接串里加上 allowPublicKeyRetrieval=true。还有一个常见表现是The server time zone value 'GMT+08:00' is unrecognized,这是时区问题,连接串加 serverTimezone=Asia/Shanghai 即可。
问题二:MyBatis-Plus 分页失效
接口返回的分页对象里 total 为 0,或者查询结果没有执行 limit,最常见的原因是 MybatisPlus 分页插件没有注册。SpringBoot2 环境下如果使用的是分页插件拦截器,一定要确认配置类被 Spring 扫描到。如果插件配置类在启动类所在包的子包之外,可以手动在启动类上添加 @MapperScan 或 @ComponentScan 注解。这个问题的排查思路是:看控台有没有打印带 LIMIT 字样的 SQL,有说明插件生效,没有说明插件没注册成功。
问题三:Vue3 前端跨域
开发环境下,前端地址是 http://localhost:5173,后端接口是 http://localhost:8080,浏览器会拦截跨域请求。解决方法有两种:第一种是在后端写一个 WebMvcConfigurer 配置类,重写 addCorsMappings 方法,配置允许的跨域来源;第二种是在 Vite 配置里写 proxy 代理,把 /api 开头的请求转发到后端端口。我推荐第二种方式,因为前端代理在生产环境部署时可以通过 Nginx 同样配置来实现,一套思路走到黑,而且不需要后端代码做额外处理。
Vite 代理的配置示例:
// vite.config.js export default { server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }配好之后,前端接口请求地址统一写/api/xxx,开发环境由 Vite 转发,生产环境由 Nginx 转发,后端 Controller 的 @RequestMapping 路径保持一致就行。
问题四:axios 请求拦截器里拿不到 token
每次请求自动带 token 的逻辑写在请求拦截器里,从 localStorage 读取。但要注意,如果 token 过期或本地没有 token,有些接口依然会被发出去,后端返回 401 后响应拦截器需要处理这个状态码:清理本地存储,并跳转到登录页。这个逻辑如果漏写了,会出现页面跳转正常但所有接口请求依然在控制台报错,或者登录过期后用户还在当前页面白屏的情况。我在实际开发中习惯把 401 处理封装成一个函数,在响应拦截器里统一调用,不要散落在各个页面。
问题五:本地启动时端口占用
SpringBoot2 默认占 8080 端口,如果本地已有其他服务占用,启动会直接报端口冲突。排查命令:
# Windows netstat -ano | findstr 8080 # Linux / Mac lsof -i :8080找到对应 PID 后杀掉进程,或者在 application.yml 里换一个端口。如果前后端都在本机调试,我建议直接把后端端口改成 8081 之类的非默认端口,省得和别人共享机器的时候互相打架。
4.2 项目上线前的检查清单
如果你希望这套养老院系统不只是停留在本地开发,而是真正部署到服务器上,下面这个检查清单是我自己项目上线前都会过一遍的,每次都能筛出一些问题。
后端方面,第一件事是关掉 SpringBoot 的调试日志级别,生产环境用 INFO 或 WARN 就够,避免日志文件被写入大量调试信息。第二件事是配置好 knife4j 或 swagger 接口文档的访问权限,防止生产环境暴露所有接口信息。第三件事是数据库参数要调整,比如连接池大小、超时时间,这些配置在本地开发时无所谓,但线上并发一上来就很有所谓了。
前端方面,用 Vite 构建的命令是npm run build,产物会输出到 dist 目录。部署时你可以用 Nginx 托管前端文件,同时反向代理后端的 API 请求。Nginx 的配置可以参考:
server { listen 80; server_name your-domain.com; 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:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有两个关键点。一个是try_files $uri $uri/ /index.html;必须配置,否则 Vue Router 使用 history 模式时,用户刷新任意非首页路由都会出现 404;另一个是/api/前缀转发到后端端口,这个要和前端封装的 axios baseURL 保持一致。生产环境的跨域问题,就是通过 Nginx 反向代理天然解决的,前后端属于同源请求。
数据库备份策略也是上线前必须考虑的。MySQL8.0 最实用的备份方式是用 mysqldump:
mysqldump -uroot -p eldercare > backup_$(date +%Y%m%d).sql配合 crontab 定时任务,每天凌晨把数据库备份到服务器磁盘,再同步一份到对象存储或异地机器,就基本可靠了。千万别把所有鸡蛋放在一个篮子里,我曾见过服务器磁盘坏了导致所有备份数据一起没了的案例。
实用建议:把这个系统的源码跑通之后,不要止步于运行起来。建议你亲手把老人档案模块的表结构改一改,比如增加一个“紧急联系人”表,把原来的单个家属电话改成多联系人结构,然后从前端页面到后端接口全链路改一遍。这样一轮实操下来,你对前后端交互、数据库设计、权限控制的掌握程度,会远超单纯把源码运行起来的效果。
我个人做这类项目的体会是,源码本身只是一个起点,真正有价值的是读懂它设计上的取舍和实现上的思路。SpringBoot2 加 Vue3 这套组合,在目前的环境下依然是学习 Java Web 全栈开发的黄金选型,它既不会像 SSM 纯前后端不分离那样让人感觉跟不上时代,又能保证足够多的参考资料和成熟方案。拿到所谓“含文档”的源码后,耐心把文档里描述的模块一个个跑通,再尝试改一个功能点、加一个模块,这个过程的收获会非常大。
最后分享一个我在实操中反复验证过的小技巧:这种管理系统项目,建议你在本地把代码跑通之后,再用它做一次从零搭建的练习,不看源码,只参考表结构和接口文档,一步步从 SpringBoot 工程初始化写到前端页面联调。走完这一轮,你对这套技术栈的掌握程度和动手能力,已经可以应对大部分初级全栈开发的日常工作场景了。