
做后台管理系统Java搭配SpringBoot、Vue3、MyBatis、MySQL这套组合已经算是当前前后端分离项目里相当主流的玩法了。最近我刚好完整地开发并复盘了一套敬老院管理系统从业务调研、数据库设计、后端接口开发到前端页面联调再到最后的部署上线整个流程走了一遍中间踩了不少坑也把很多以前一知半解的点彻底理清了。这篇博客把这套系统的完整设计思路、核心实现细节、环境搭建过程以及我实际遇到过的坑全部整理出来适合正在做Java毕业设计、想系统学习前后端分离项目开发或者刚入门SpringBoot和Vue3想找一个完整项目练手的朋友参考。我会尽量把每个关键选择背后的原因讲清楚而不是只丢一堆代码让你自己看。1. 敬老院管理系统的需求拆解与整体架构1.1 为什么选敬老院管理这个业务场景很多人不理解做项目练手为什么不选电商、选博客非要选敬老院管理这种看起来不那么“时髦”的业务。我实际做下来发现敬老院管理系统的业务复杂度非常合适它不像电商那样有大量并发和促销逻辑但又不至于像单表CRUD那样没什么技术含量刚好处在一个很舒服的中间位置。敬老院的日常管理涉及老人的基本信息、入住退住流程、床位分配、护理记录、健康档案、费用结算、家属沟通、护工排班、访客登记等多个环节每个环节之间有明确的数据流转关系。比如一个老人入住首先要提交入住登记信息然后系统要分配床位紧接着要给老人建立健康档案最后还要关联到后续的护理计划和费用账单。这串流程做下来正好能把SpringBoot的后端接口设计、MyBatis的动态SQL、MySQL的表关系设计都练得很扎实。更重要的是这类业务系统对权限非常敏感。管理员、护士、护工、财务人员、家属不同角色能看到的数据和能执行的操作是完全不一样的。这逼着我去认真做登录认证、角色权限控制而不是像很多示例项目那样只做一个假登录。这一点在面试或者答辩的时候都是非常加分的亮点。1.2 功能模块划分与数据流转我把系统拆成了七个核心模块这也是大多数类似管理系统可以复用的骨架老人档案管理负责入住老人的基本信息、家属联系方式、身份证件、健康状态等数据维护是整个系统的数据基础。床位管理维护敬老院内的房间和床位资源支持空闲、占用、维修三种状态入住时自动锁定床位退住时释放床位。护理记录管理护工按老人维度填写每日护理情况包括饮食、睡眠、体征、特殊护理事项按时间倒序展示。健康档案管理记录老人的既往病史、过敏药物、体检数据支持按时间线查看健康变化。费用管理根据护理等级、住宿标准、餐饮天数自动生成月度账单支持线下缴费后的状态确认。访客登记管理登记来访人员信息、被访老人、到访时间形成可追溯的访客记录。系统管理用户管理、角色管理、菜单权限配置是前后端分离项目里权限控制的核心实现部分。模块之间的数据流转是这样的一名老人入住时触发床位状态变更同时生成健康档案和护理计划护工每天填写护理记录后会累积到老人的护理档案里费用结算模块每个月读取老人的住宿天数、护理等级和餐饮数据自动计算出本月应缴费用。这种联动逻辑在数据库层面会拆成多张表通过外键和业务代码保证一致性。1.3 前后端分离架构与目录组织这套系统采用前后端完全分离的架构后端只提供RESTful API接口前端通过HTTP请求获取JSON数据并渲染页面。前后端通过接口文档约定字段格式开发时可以并行推进互不阻塞。后端用SpringBoot搭建目录上采用经典的分层结构Controller层负责接收请求和参数校验Service层处理业务逻辑Mapper层通过MyBatis与MySQL交互。entity目录放数据库实体对象dto目录放接口传输对象vo目录放视图对象common目录放统一返回结果、全局异常处理、分页参数等公共组件。前端用Vue3 Vite Element Plus构建目录上按功能划分api目录封装所有的HTTP请求router目录配置页面路由views目录按功能模块存放页面组件store目录用Pinia管理全局登录状态和用户信息utils目录封装axios实例和通用工具方法。实际开发中我强烈建议前后端都统一好返回结果的格式我的习惯是所有接口都返回一个Result对象结构是code、message、data三个字段。code为200表示成功非200表示业务异常这样前端封装axios拦截器时只需要根据code做一次统一处理省去大量重复的错误判断代码。2. 技术选型解析SpringBoot、Vue3、MyBatis、MySQL各司其职2.1 后端为什么用SpringBoot而不是传统SSM很多人在JavaWeb阶段学过SSM框架也就是Spring SpringMVC MyBatis分别配置三大段XML光配置文件就能写半天。SpringBoot最大的价值是解决了配置地狱问题把SSM整合过程中那些繁琐的bean注册、组件扫描、数据源配置全部做成自动装配我只需要引入对应的starter依赖再在application.yml里写上数据库连接信息就能直接启动。在敬老院管理系统这类业务系统里SpringBoot的起步依赖非常省心。引入spring-boot-starter-web就完成了内置Tomcat和SpringMVC的整合引入mybatis-spring-boot-starter就自动配置好了SqlSessionFactory和Mapper扫描。省下来的时间可以全部用在业务逻辑上这是SpringBoot成为Java后端主流框架的根本原因。另外SpringBoot在部署层面也友好很多一个mvn package就能打出可直接运行的jar包服务器上有JDK环境就能跑不需要额外装Tomcat。对个人项目或者小型系统来说这种部署体验其实非常重要。2.2 Vue3 Vite Element Plus的前端组合前端选择Vue3而不是Vue2主要是Vue3的Composition API让代码组织方式更灵活尤其是当页面的业务逻辑比较复杂时可以把关联的逻辑抽到自定义函数里复用而不是像Options API一样强制按data、methods、computed分类散落。实际开发中我用的是Vue3的setup语法糖配合ref和reactive管理响应式数据。以一个养老人员列表页面为例查询条件、分页参数、表格数据、加载状态是需要联动的一整组逻辑用Composition API可以把这些逻辑都封装进一个useElderList的函数里组件代码会显得非常干净。Vite替代Webpack是我非常推荐的选择在开发环境冷启动速度和热更新体验上完全不是一级别的。Vue3官方脚手架create-vue默认就是Vite驱动直出的项目结构干净没有传统Webpack项目那种大量看得人头疼的配置文件。UI组件库我选了Element Plus它是Vue3技术栈最活跃的组件库。表格、表单、弹窗、分页、日期选择器这些后台管理系统的高频组件全部开箱即用。这里给新手一个建议不必追求手写所有组件管理系统的价值核心在业务逻辑和数据处理界面上用成熟组件库能极大提高开发效率。2.3 MyBatis的定位与MySQL表设计要点MyBatis在这套系统里的角色是持久层框架负责Java对象和数据库记录之间的映射。我是比较喜欢MyBatis的因为SQL是自己写的可控性很强遇到复杂的多表联查、条件动态拼接写XML里的SQL比各种框架封装的方法更直观。MySQL是底层的数据存储重点在于表的结构设计。敬老院管理系统十几张表建下来我有几个很深的体会第一主键一律用自增ID不要用UUID做大批量数据的主键因为UUID是无序的在InnoDB引擎下会导致频繁的页分裂写入性能差。我自己在业务里需要暴露给外部的编号比如老人编号、账单编号就单独设计一个带业务含义的字段。第二表示状态的字段尽量用tinyint而不是varchar比如床位状态0空闲、1占用、2维修在代码里用枚举或常量维护语义。虽然直接用中文存储看起来更直观但状态字段用数值存储效率更高且不容易写错前端展示时再转换成对应的标签。第三关联查询频繁的字段一定要加索引。比如护理记录表按老人ID和记录日期查询就在老人ID和记录时间上建联合索引。这个投入产出比非常高数据量上来之后查询速度差别很大。第四金额字段用decimal而不是float或double。费用管理要涉及月度账单浮点数在二进制下无法精确表示累计多次会出现微小误差对账的时候非常麻烦。decimal存的是精确小数做金额计算是必须的选择。3. 核心功能模块的实现细节与实操过程3.1 老人档案与入住管理的关键设计老人档案是整个系统的数据源头我设计的主体表是elder表字段包括老人姓名、性别、出生日期、身份证号、联系电话、紧急联系人、联系人与老人关系、入住状态、入住时间、所属床位ID等。身份证号在业务上是敏感数据做列表查询时不应该直接返回完整号码我的方案是接口返回脱敏后的字符串前端展示也是处理后的版本。入住流程是最能体现业务设计的部分完整流程是先录入老人的基础信息保存后状态为入驻中然后系统展示可供分配的床位列表管理员根据老人需求选择房间和床位选完床位后状态变为已入住同时自动创建健康档案记录并把初始护理等级写入护理计划表。这个流程在代码上有几个需要注意的点床位分配操作必须在事务中执行因为要同时更新老人表、床位表、健康档案表任何一个步骤失败都要回滚否则会出现老人已绑定床位但床位状态还是空闲的数据不一致。我这里直接用Transactional注解搞定的同时在写操作之前要对床位状态做一次校验确认目标床位确实处于空闲状态才能执行分配。还要考虑一个重复入住的问题。我在elder表上对身份证号做了唯一约束这样当用户尝试为一个已经在院的老人再次办理入住时数据库层面会直接拦截。不过我也在Service层做了前置校验提前给前端返回友好提示而不是等数据库报一个难看的DuplicateKeyException。3.2 护理记录与健康监控的实现要点护理记录是护工每天操作最频繁的功能核心表是nursing_record字段包括老人ID、护理人员ID、护理日期、饮食情况、睡眠情况、体温、脉搏、特殊护理内容、备注。每条记录对应一天内的一次护理行为老人一天可以被多个人多次护理所以记录之间通过老人ID关联汇总成老人的护理时间线。这里我做了两个有代表性的设计一个是MyBatis动态SQL的思想在实际业务中的运用。护理记录查询页面的筛选条件有老人姓名、护理日期范围、护理人员姓名用户可能只输入其中一个条件查询也可能三个条件同时输入。如果用java代码去拼接SQL代码会非常臃肿。我在Mapper的XML里用了if标签动态拼筛选条件效果很干净select idlistRecords resultTypecom.example.vo.NursingRecordVO SELECT nr.*, e.name AS elderName, u.name AS nurseName FROM nursing_record nr LEFT JOIN elder e ON nr.elder_id e.id LEFT JOIN sys_user u ON nr.nurse_id u.id where if testelderName ! null and elderName ! AND e.name LIKE CONCAT(%, #{elderName}, %) /if if teststartDate ! null AND nr.nursing_date gt; #{startDate} /if if testendDate ! null AND nr.nursing_date lt; #{endDate} /if /where ORDER BY nr.nursing_date DESC, nr.create_time DESC /select动态SQL的好处是这段代码能应对所有查询场景用户在页面上选择几个条件就拼几个条件不选就查全部一个方法通吃。第二个是健康监控的时间线设计。健康档案表health_record记录了老人的体检结果和健康事件我把每次操作都视为一次时间线节点展示时按时间倒序排列。界面做成纵向时间线组件一眼就能看出老人最近的健康状况变化这种展示方式在实际使用中很受欢迎。3.3 费用结算模块的计算逻辑费用模块是这套系统里业务逻辑最绕的部分涉及的费用类型有住宿费、护理费、餐饮费、其他费用。住宿费和护理费是按月度固定标准计算的餐饮费按实际用餐天数计算另外还会有药品费、日用品费这类临时费用。我的费用表设计是fee_bill和fee_item两张表fee_bill是账单主表记录老人ID、账单月份、总费用、缴费状态fee_item是账单明细表记录每条费用项目的类型、项目名称、金额、备注。一个账单对应多条明细这种主从表结构在查询账单详情的时候非常好用避免在单表里塞一堆冗余字段。月度自动生成账单的逻辑我放在Service层通过定时任务触发在每月1号扫描所有状态为在住的老人找出上月不在账单生成范围内的老人ID再结合老人护理等级对应的收费标准、上月的住宿天数、餐费标准逐条生成费用明细最后汇总出账单总金额。这里有个非常容易出错的细节费用的计算必须依据固定历史数据不能实时去查老人当前的护理等级和床位价格。因为老人的护理等级可能会调整床位价格也可能变动如果生成账单时实时查询就会导致已经生成的账单跟着变化数据对不上账。我的做法是在生成费用明细的那一瞬间把护理等级名称、单价、住宿天数作为快照字段存进fee_item表这样历史账单永远稳定不变。3.4 权限管理从登录到菜单鉴权权限管理我基于SpringBoot整合Sa-Token来实现理由是这个框架上手成本远低于Shiro和Spring SecurityAPI设计直观文档也很完善。登录接口的大致流程是接收账号密码查询sys_user表校验密码密码在数据库里存的不是明文而是BCrypt加密后的哈希值匹配通过后就创建一条会话token返回给前端。密码加密这里特意强调一下入职后我见过很多老系统直接明文存密码这是非常大的安全隐患。只要数据库泄露所有用户的账号就彻底暴露了。BCrypt加密自带随机盐即使用户设置相同密码生成的哈希值也不同能有效应对彩虹表攻击。Spring Security框架内置了这个工具我这里单独引入jBCrypt库来实现。前端路由用路由守卫配合动态菜单做权限控制。用户登录后后端返回该用户能访问的菜单列表和权限标识前端把这部分数据写入Pinia并动态生成侧边栏菜单。路由守卫在跳转前检查用户是否已登录未登录一律重定向到登录页已登录但当前路由不在权限列表里的则提示无权限。实际开发中踩过的一个坑是早期我把菜单写死在前端结果发现不同角色登录后虽然看不见无权访问的菜单但直接通过URL还是能跳到对应页面。这就暴露出一个问题前端隐藏菜单只是交互层面的控制真正的权限控制必须在后端接口上做。后来我给后端的敏感接口都加了SaCheckPermission注解权限校验才算闭环。4. 环境搭建与前后端联调实战4.1 从零搭建后端工程后端工程我建议直接用Spring Initializr创建可以选择使用IDEA的Spring Initializr也可以直接访问start.spring.io生成项目压缩包。需要勾选的依赖有Spring Web、MySQL Driver、Validation。MyBatis依赖和Sa-Token依赖在Initializr里没有提供需要在pom.xml里手动引入坐标。application.yml是我每次搭建项目时最注意的文件数据库连接、MyBatis配置、日志打印都在这里server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/elder_home?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplurl里我特别加了serverTimezoneAsia/Shanghai这是很多新手容易忽略的坑。MySQL5.6以上版本默认时区跟中国时区有偏差不加这个参数LocalDateTime写入数据库后再查出来会差8个小时。map-underscore-to-camel-case这个配置也很重要它能让数据库的snake_case字段名自动映射到Java的camelCase属性名。比如数据库字段create_time会自动对应Java实体里的createTime属性省去大量手动resultMap映射工作。搭建MyBatis时要注意mapper-locations的路径配置必须确保XML文件所在的目录能被Maven打包进去。有时候用IDEA直接运行时一切正常但打成jar包后却提示Invalid bound statement就是因为XML文件没被包含进构建产物。解决办法是在pom.xml的build节点里把resources包含进来。4.2 前端工程创建与代理配置前端我使用npm create vitelatest命令创建Vue3项目模板选择vue。之后安装element-plus、axios、pinia、vue-router这几个核心依赖。Element Plus的引入方式有两种选择一个是完整引入一个是按需引入。完整引入适合个人项目配置简单按需引入能显著缩小打包体积但对工程配置要求高一些。我自己的项目为了省事用的完整引入如果你们做的是生产级别项目建议按需引入。开发环境下最核心的配置是axios的baseURL和Vite的代理。前后端分离开发时前端跑在5173端口后端跑在8080端口直接请求会触发跨域。我用Vite的proxy配置把请求转发到后端服务具体配置写在vite.config.js里import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端所有请求都写成以/api开头的相对路径例如axios.post(/api/elder/list)。开发环境由Vite代理转发到后端生产环境则用Nginx配置反向代理后端接口。这种写法解耦了前后端的接口地址环境切换时不需要改前端代码。axios的封装里我加了请求拦截器和响应拦截器。请求拦截器负责把登录后存的token加到请求头的Authorization字段响应拦截器统一处理返回结果。如果code字段是401表示登录已过期直接清空本地登录信息并跳转登录页如果是其他业务错误code就弹个ElMessage提示错误信息。这样业务代码里只需要处理正常数据异常场景全部交给拦截器兜底。4.3 数据库初始化与初始数据准备数据库设计我直接用Navicat可视化建表的建完后用SQL文件保存表结构和初始数据。初始化数据里必须包含一个管理员账号不然系统第一次登录都没有入口。我的做法是写一个SQL脚本用BCrypt加密后的固定字符串作为管理员初始密码存入数据库。一个值得借鉴的习惯是建表SQL里除了字段定义外还要写好基础索引和关键外键关联。以费用账单表为例我建了idx_elder_month联合索引后续月份结算、老人账单查询都依赖这个索引。项目第一次启动前要先把SQL脚本导入MySQL然后检查application.yml里的账号密码和实际数据库一致否则后端启动时会报Access denied。如果启动了Test数据源连接检测IDE控制台会出现红色错误但那不影响应用继续启动只要数据库配对了就行。另外我再补充一个小建议新建数据库时一定要将字符集和排序规则选成utf8mb4和utf8mb4_general_ci。utf8mb4能完整支持emoji和一些特殊汉字老的项目有的用了utf8遇到生僻字存储会报错这类问题排查起来非常隐蔽。5. 我踩过的坑常见问题与排查实录5.1 跨域问题一次CORS排错笔记开发初期前端调登录接口就卡住了浏览器控制台报错Cross-Origin Request Blocked。虽然我配置了Vite的proxy但排查下来发现是代码把请求路径写死了axios的baseURL设置成了http://localhost:8080绕过了代理导致请求从5173端口直接发到8080端口触发CORS。这个问题有两种解决办法一种是把axios的baseURL改成/api让请求走代理转发另一种是在后端加一个CORS配置类允许指定来源的跨域请求。我的建议是以代理方案为主因为后端你无法控制调用方的来源开放跨域本身就存在安全风险。如果一定要在后端配置也必须把allowedOrigins限定到具体的地址和端口不能直接写星号。5.2 MyBatis分页插件失效的真相费用账单列表的分页功能在联调时出现了一个诡异的现象第一页数据显示正常点击第二页后返回的还是第一页数据。排查了很久最后定位到是PageHelper分页插件使用方式的问题。由于项目使用的是MyBatis分页插件PageHelper这是一个基于拦截器实现的物理分页组件它会拦截执行查询的方法自动把分页参数拼接到SQL上去。但这里有个使用前提必须是PageHelper.startPage()调用后紧接着执行的那条查询语句才能被拦截并分页。我在Service层写代码时startPage后面先执行了一条count查询又执行了一条主查询结果PageHelper把分页信息作用到了count查询上主查询没被分页拦截于是每页查询结果都一样。修改方式是调整语句顺序让startPage后面紧跟真正的列表查询并且因为列表SQL返回的是带left join的多表查询结果PageHelper完成分页后还会自动生成count查询语句。分页对象返回的是Page类型我需要把列表数据和总条数取出来封装到统一的分页返回VO里这个习惯在生产项目中很重要避免前端获取total字段时取到空值。5.3 LocalDateTime序列化与前端日期显示前后端联调时发现一个问题接口返回的老人入住时间是一长串数组类似2024, 12, 11, 10, 30, 0前端没法直接展示成正常格式。原因是我在实体类里用了LocalDateTime类型而后端默认的JSON序列化器对LocalDateTime的序列化结果是这种结构这不是可读的日期字符串。解决办法是统一配置Jackson的日期序列化格式我写了一个Jackson配置类全局指定LocalDateTime统一输出成yyyy-MM-dd HH:mm:ss格式。前端拿到字符串后直接在表格里展示即可必要时用dayjs转成自己想要的显示格式。另外还有人会使用JsonFormat注解来设置字段格式这适合局部字段的格式化处理。全局配置和注解配置各有使用场景个人项目里我倾向于全局配置因为所有接口的表现都统一不会出现有人用了注解有人忘了加的情况。5.4 表字段命名规范引发的映射错误MyBatis的一个常见坑是字段名映射问题。如果数据库字段是create_timeJava实体类属性是createTime而MyBatis配置里没有开启驼峰映射那么查询结果里这个字段就是null。我在开发早期遇到过后来在application.yml里加上了map-underscore-to-camel-casetrue一个问题就全解决了。但有一个特例必须注意如果SQL语句中给字段起了别名比如select create_time as createDate别名和Java属性名不一致时驼峰映射依然生效不了需要resultType映射的时候检查字段别名。我在一个统计报表里就犯过这种错误排查了很久才发现是别名的问题。建议在实体类字段命名和数据库字段命名保持一致的对应关系能省掉很多不必要的麻烦。除了上面这些还有几个我实际体验很深的点也一并整理。比如MySQL连接数据库时加参数allowPublicKeyRetrievaltrue和useSSLfalse很多本地环境连接失败都是因为这两个参数没设置好。再比如前端ElTable渲染长列表时一定要给表格列设置固定的宽度或者用show-overflow-tooltip处理否则中文姓名和长备注会把表格撑变形这个小细节非常影响使用观感和体验。最后分享两个我在这个项目里觉得特别值得的小技巧第一个技巧是日志的重要性。开发阶段我把MyBatis的log-impl配置成了StdOutImpl控制台会完整打印每一条SQL和参数。排查数据错误时一眼就能看出SQL语句和预期的差别这比反复看代码猜测要快得多。很多新手遇到查询结果不对就去翻代码实际上问题往往就在SQL层面多看日志比多猜代码有效。第二个技巧是统一异常处理。我的后端加了一个全局异常处理器针对参数校验异常、业务异常、未知异常分别做处理返回给前端可读的错误提示。开发期间我觉得这个配置有点多余后来联调时才体会到它的好处前后端约定好错误码格式后所有业务错误都能明确显示给用户而不是控制台里一堆红色的堆栈信息前端只能看到一个500。这套敬老院管理系统做下来整体收益非常大。它不像纯理论教程那样学过就忘而是一个真正能跑起来的完整项目从业务到技术栈都是主流实战方案。如果你也想用SpringBoot、Vue3、MyBatis这套技术栈练手强烈建议找这样一个业务闭环完整的系统去做远比跟着视频敲一个没有业务逻辑的CRUD案例有价值。做项目途中遇到的问题往往才是真正让你进步的地方。