SpringBoot+Vue养老管理系统:从需求设计到部署避坑全解析

📅 发布时间:2026/10/18 5:24:10
SpringBoot+Vue养老管理系统:从需求设计到部署避坑全解析
做养老管理系统这几年陆续接过不少类似的活儿但看到“夕阳红公寓管理系统”这个标题的时候还是停下来多看了两眼。原因很简单养老类管理系统和普通的企业管理系统差别很大它的核心不在“管理”而在“照护服务链路”的完整性。一个老人从入住评估、房间分配、健康档案建立到日常护理记录、费用结算、亲属沟通整条链路必须严格闭环。任何一个环节断掉系统就成了摆设。这篇博文就基于我在类似项目里的实操经验把这套基于SpringBootVueMySQLMyBatis的完整实现拆开揉碎讲清楚从需求梳理到数据库设计从后端接口到前端页面最后再到部署踩坑全程复盘。1. 项目需求梳理与模块边界划分很多新手拿到这类项目第一反应是赶紧建表写接口实际上这是最容易翻车的做法。养老管理系统最忌讳的就是“功能堆砌”做了一堆页面真正用起来却发现业务流程走不通。我在动手之前通常先花半天时间把角色和核心流程捋清楚。1.1 角色权限模型这套系统的角色其实比一般的后台管理系统复杂一些需要考虑实际业务场景里的几个关键角色系统管理员、前台接待员、护理人员、护士/医生以及老人家属通常只读。每个角色看到的界面和能操作的数据完全不一样。我见过很多毕业设计或者外包项目的做法是搞一个大而全的权限表然后每个接口都写一堆if else判断角色后期维护起来非常痛苦。这里我更推荐用Spring SecurityJWT做基于角色的访问控制在拦截器里统一校验前端再配合Vue Router的路由守卫做菜单级控制。前端控制是体验层的后端控制才是安全层的两者缺一不可。1.2 核心业务流程闭环这套系统的核心业务链路可以概括成一条线入住申请→健康评估→分配房间→建立档案→日常照护护理记录/用药提醒/健康巡检→费用结算→退住归档。这里有个很容易被忽略的设计点健康评估。很多系统把评估做成一个简单的表单但实际上老人的健康状况是动态变化的每隔一段时间要重新评估。所以我在设计数据库的时候评估记录会单独建一张表跟老人基本信息表分开这样后续可以追溯每次评估的历史。费用结算也是一个容易出问题的地方。老人的费用构成跟酒店入住完全不同除了床位费、餐费还有护理费按护理等级区分、医疗费、日用品代购费等。我设计的是一个费用项目表加一个账单明细表的方式每个费用项可以单独配置价格月度账单通过后台任务自动生成。用户手动去录入这么大一张账单一来容易出错二来工作量也不可控。2. 数据库设计实战二十一张表的拆解思路这套系统我维护过一版完整的库结构总共二十六张表核心的业务表其实是下面这几类。我挑几张最有代表性的拿出来说。2.1 老人档案与健康数据表老人基本信息表elder_info是所有业务的核心。但这里容易踩坑的点在于老年人信息里有相当一部分高敏字段比如身份证号、既往病史、紧急联系人电话等。我的建议是这些字段单独存一张表elder_private_info通过主键关联查询的时候按需取用。这样做的好处一是安全合规二是减少主表宽度查询性能更好。健康数据表我拆成了三张独立表健康评估表、体检记录表、用药记录表。评估表存每次的分项评分伙同评估人和下次复评日期。体检记录表存的是历次体检的检查结果字段类型上血红蛋白、血压值这类数值建议用DECIMAL而不用VARCHAR不然以后做趋势分析的时候还得CAST一遍。这里有个经验很实用血压这种数据不要只存一个值进去。收缩压和舒张压是两个独立字段对应界面上的两个输入框。一个input里塞两个值的做法很省事但后面做数据筛选和趋势图的时候直接想哭。2.2 房间与床位管理房间表room和床位表bed分开设计这个几乎是刚需。因为实际场景中会出现一个双人间住了一个老人、另一个床位空着的情况如果直接按房间绑定老人逻辑上就有漏洞。我用两个表加一个当前入住状态字段来管理房间状态自动汇总床位状态生成设计上简洁很多。2.3 费用与账单费用项表charge_item维护系统的价格字典比如“一级护理费日”“双人间床位费日”。账单明细表bill_item记录老人每天产生的费用条目。为什么按天记录而不是按月记录因为入住天数并不总是满月退房当天的费用结算还得按比例算。按天生成虽然数据量大一些但换来的是逻辑的确定性和准确性其实很值。3. 后端接口设计与业务逻辑落地数据库设计好了后端写代码就有底气了。SpringBoot项目的分包结构我习惯按功能模块分包而不是按技术层分包不然一个几十张表的项目controller、service、mapper目录下动辄几十上百个文件找东西全靠瞪眼。3.1 统一返参与异常体系这块是很多新建工程的“隐藏分”。我见过不少项目接口返回格式五花八门有的返回Map有的直接返回实体类前端拿数据还得猜。统一的Result对象是必须的并且在构造它的初始阶段就要把业务异常码体系设计好。我在这套系统里定义的错误码分为几组1xxx是参数问题2xxx是业务规则问题比如床位已占用3xxx是权限问题5xxx是系统异常。前端拿到code就可以直接弹对应的错误提示后端也不用把报错的堆栈信息直接甩给前端安全很多。注意给前端返回错误信息的时候一定要控制长度和内容范围。数据库异常信息、堆栈信息、具体的SQL语句绝对不能出现在返回体里否则等于把系统内部结构免费送人了。统一用“系统繁忙请稍后再试”这类脱敏文案详情只写在日志文件里。3.2 关键业务接口示例我挑几个核心接口来说。入住登记接口POST /api/admission是业务起点也是逻辑比较复杂的一个接口。它要同时做这几件事校验老人身份证是否已存在、校验目标床位状态、创建老人档案、创建入住记录、修改床位状态为已入住、初始化该老人的默认费用计划。这些操作如果分散在Service里的多个方法里各自调用中间任何一个环节失败数据就会不一致。我的做法是在Service方法上加上Transactional然后理清调用顺序。到这里就不得不提一个Spring事务上的老坑后面有专节讲。健康评估接口POST /api/assessment相对直观需要记录总分和各项得分同时更新老人信息表里的护理等级字段。这个护理等级直接决定后续的费用计算和护理任务分配所以在Service里需要重新查询一次评估表的最新记录而不是直接信任前端传来的老人ID对应的旧值。费用结算接口POST /api/settlement/prepare是我当时花时间比较长的一个接口。月度结算不是一个简单的SUM它要考虑入住不满一个月的情况怎么按天折算、一次性项目和日计项目的区分、退住时未结清的药品费怎么处理。我的做法是先按费用项类型拆分逻辑日计项目按入住天数乘以单价一次性项目原价原计最后统一汇总成预结算订单返回前端待用户确认后再落账。4. 前端Vue实现与后端联调的“舒适区”前端我用的Vue2技术栈加Element UI。这算是个成熟组合生态丰富、坑少、案例多非常适合这类后台管理系统的快速成型。Vue3我也用过但考虑到当前工程的整体稳定性和团队上手成本Vue2在这类场景下依然是性价比极高的选择。4.1 权限控制与动态路由前端权限这块我的做法是登录后拿到角色信息前端根据角色动态生成路由表。不是让后端把菜单返回因为那样前端还是要写一份映射关系加上一份直接关系其实维护成本一样。核心是在路由守卫里判断router.beforeEach((to, from, next) { if (!getToken()) { if (to.path /login) next() else next(/login) } else { if (!store.state.userInfo) { store.dispatch(fetchUserInfo).then(() { if (to.meta.roles !to.meta.roles.includes(store.state.userInfo.role)) { next(/403) } else { next() } }) } else { next() } } })简版的意思是让路由的meta字段声明角色限制跟后端接口的权限注解配合好两边一起管。4.2 页面组件复用养老管理系统里很多页面长得特别像比如老人列表和家属列表、房间列表和床位列表。我的做法是抽了一个通用的列表页组件配置列定义、搜索条件、分页参数传入不同的接口地址渲染不同的页面。虽然前期搭组件花一点工夫但后面新增页面基本上就是复制配置文件开发速度提升很大。还有一个经验分享表单校验不能只依赖前端。Element UI的rules写起来方便但后端Controller里的参数校验JSR-303必须同步做比如身份证号格式校验前端过了还要后端再过一遍这是底线。4.3 Axios封装与请求拦截器请求封装是前端的基建工程。我习惯在utils/request.js里统一配置axios实例设置请求拦截器带上JWT token响应拦截器统一处理错误码。这里有个容易忽略的问题文件下载和上传的接口不能走JSON的responseType要单独在请求配置里标识。不然后端返回的文件流会被axios当成JSON解析出来的是一堆乱码等到排查的时候还以为是后端接口写错了。在axios的响应拦截器里我还会统一处理token过期问题。收到401就清除本地登录态、跳转登录页、提示用户重新登录这是很基础但极其重要的一环。5. 从零到一完整的项目搭建与核心代码落地说了这么多设计层面的东西还是得落到实际代码上。我把关键实现一步步列出来方便大家直接对着搞。5.1 工程结构与依赖后端工程结构清晰分层controller只做参数接收和结果包装service负责业务逻辑mapper只做持久层操作。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependencyMyBatis的版本选择上我用的是2.x而非3.x因为这个版本跟SpringBoot 2.x的整合相当丝滑配置项少报错少。如果新开项目用SpringBoot 3.x就得换成mybatis-plus或mybatis-spring-boot-starter 3.x同时注意javax到jakarta的命名空间迁移那是另一套事了。5.2 MyBatis XML映射核心片段以“查询老人分页”为例展示XML里如何写动态条件。其实核心就是要让前端传过来的搜索参数发挥作用在上面做判断语句来拼接SQLselect idselectElderPage resultTypecom.example.vo.ElderVO SELECT e.id, e.name, e.gender, e.id_card, e.bed_id, b.room_no, b.bed_no, e.nursing_level, e.status FROM elder_info e LEFT JOIN bed b ON e.bed_id b.id where if testname ! null and name ! AND e.name LIKE CONCAT(%, #{name}, %) /if if teststatus ! null AND e.status #{status} /if if testnursingLevel ! null and nursingLevel ! AND e.nursing_level #{nursingLevel} /if /where ORDER BY e.updated_at DESC /select注意这里用了LEFT JOIN关联床位表因为老人可能暂时床位未分配新入住等待区不能用INNER JOIN把这种数据过滤掉了。5.3 业务事务管理示例入住登记接口的Service代码如下Service public class AdmissionServiceImpl implements AdmissionService { Autowired private ElderInfoMapper elderInfoMapper; Autowired private BedMapper bedMapper; Autowired private AdmissionRecordMapper admissionRecordMapper; Override Transactional(rollbackFor Exception.class) public Long admission(AdmissionRequest req) { // 1. 校验身份证唯一性 Integer count elderInfoMapper.countByIdCard(req.getIdCard()); if (count 0) { throw new BusinessException(ErrorCode.ELDER_EXISTS); } // 2. 创建老人档案 ElderInfo elder new ElderInfo(); BeanUtils.copyProperties(req, elder); elderInfoMapper.insert(elder); // 3. 床位状态更新 Bed bed bedMapper.selectByIdForUpdate(req.getBedId()); if (bed null || !EMPTY.equals(bed.getStatus())) { throw new BusinessException(ErrorCode.BED_NOT_AVAILABLE); } bed.setStatus(OCCUPIED); bedMapper.updateById(bed); // 4. 创建入住记录 AdmissionRecord record new AdmissionRecord(); record.setElderId(elder.getId()); record.setBedId(bed.getId()); record.setAdmissionTime(new Date()); admissionRecordMapper.insert(record); return elder.getId(); } }这段代码里有几个细节值得单独说。第一selectByIdForUpdate用了悲观锁去锁床位记录。为什么要锁因为在并发场景下两个操作员同时给两个老人分配同一张床位如果不加锁两个请求都查询到EMPTY状态然后同时更新床位就会被重复分配。用一个for update锁住当前事务内的这一行第二个请求必须等第一个事务提交才能执行查询从源头上避免了超卖。第二业务异常BusinessException是自定义RuntimeException配合全局异常处理器一起工作比较简单干净。第三事务的rollbackFor设为Exception.class作用是让任何异常都触发回滚。千万不能省这个参数直接用默认配置SpringBoot的默认设置是仅RuntimeException和Error才回滚是不是受检异常的交易异常直接导致数据残留真的很伤。6. 几个坑不吐不快实操中最容易翻车的位置这部分是我压箱底的避坑经验每一条都是真金白银踩出来的。6.1 MyBatis一二级缓存引发的“幽灵数据”MyBatis的二级缓存默认在单个SqlSessionFactory范围内开启。听起来很美好但在这个系统里一个突出场景是操作员先在A页面修改了老人状态如转床然后在B页面查询列表发现状态还是旧的。怎么办原因往往就是二级缓存了旧查询结果没有失效。我的处理方式是系统全局关掉二级缓存用Spring的声明式事务加数据库的默认隔离级别来控制数据一致性。实际上对于这类管理系统数据库压力本身不大性能瓶颈不在缓存而在SQL写得好不好、索引建没建对。缓存省下的那点开销不值得拿数据一致性来换。6.2 LocalDateTime与Jackson序列化前后端联调时时间字段是重灾区。数据库datetime对应Java的LocalDateTime返回给前端是ISO格式的字符串前端如果不做格式化表格里显示一堆带T的字符串观感很差。我的做法是全局配置Jackson的序列化格式在application.yml里统一处理spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8与此同时前端在接收时约定好解析。两边各管一段格式统一了后面的按时间筛选、按日期分组就会非常顺畅。6.3 批量插入与MySQLURL参数配置批量导入老人信息比如从Excel导入时很多人喜欢循环调用单条insert两千条数据要跑几分钟被人吐槽“系统真卡”。实际上MyBatis的foreach批量提交是标配insert idbatchInsert INSERT INTO elder_info (name, gender, id_card, birthday, phone, create_time) VALUES foreach collectionlist itemitem separator, (#{item.name}, #{item.gender}, #{item.idCard}, #{item.birthday}, #{item.phone}, NOW()) /foreach /insert但这里有个隐藏前提MySQL连接URL上需要加上allowMultiQueriestrue不然MyBatis生成的多条语句拼接还是会被拒掉。这个参数不加批量插入就是“看起来写对了实际跑不通”。6.4 数据权限的“内鬼”风险管理后台的数据权限是个容易被忽视的点。比如系统里有多个分店每个店长只能看自己店内的老人数据但技术人员图省事直接写select * from elder_info。表面上看功能是好的实际上哪个分店的数据都能串着看到这是管理上的大忌。我在这套系统里加了一层数据隔离所有业务表都带上branch_id字段查询的时候通过MyBatis拦截器自动拼上当前登录用户的branch_id条件从语义上杜绝了跨店数据越权。虽然只对多店场景有效但这是我强烈建议做的设计决策。7. 项目打包部署与上线要点本地开发环境跑通不代表能顺利部署到服务器。这里是部署流程和几个容易踩的坑。7.1 后端打包启动我用的打包配置是SpringBoot Maven插件打包成可执行jar包部署。有几点值得注意生产环境的数据库连接信息、Redis地址等应该外置到application-prod.yml里通过启动参数指定激活的profile。配置里绝对不能写明文数据库密码至少用jasypt做可逆加密更稳妥的是接入配置中心。bash启动命令nohup java -jar sunhong-apartment.jar \ --spring.profiles.activeprod \ --server.port8080 \ app.log 21 JVM参数里我会显式加上时区和内存配置。为什么要指定时区因为云服务器默认是UTC时区而数据库里存的是北京时间不统一的话日期时间全部错乱八小时排查起来极其抓狂。nohup java -jar -Duser.timezoneAsia/Shanghai -Xms512m -Xmx1024m sunhong-apartment.jar app.log 21 7.2 Nginx反向代理配置前端构建后产物是dist目录下的纯静态文件由Nginx托管同时通过反向代理把/api前缀转发给后端服务。nginx的location路径匹配有一个很经典的坑/api会被也匹配上/api/login等任何以/api开头的地址。所以不能用精确匹配要用前缀匹配同时保留路径。我用的是location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }proxy_pass最后带不带斜杠坑太多。带斜杠表示将匹配路径中的/api/部分替换为根路径不带斜杠则原样转发。这个点曾经让我排查了大半个下午就为了某几个接口404的问题。7.3 前端打包优化Vue项目的打包优化核心是拆包和压缩。我在vue.config.js里配置webpack的splitChunks把element-ui、axios这类公共库独立分包缩小首屏加载体积module.exports { publicPath: ./, productionSourceMap: false, configureWebpack: { optimization: { splitChunks: { chunks: all, cacheGroups: { elementUI: { name: element-ui, test: /[\\/]node_modules[\\/]element-ui[\\/]/, priority: 10, chunks: all } } } } } }publicPath这里有个细节需要注意如果用相对路径(./)部署到服务器的任意子目录下都只需要改Nginx配置不需要重新打包前端项目很适合经常需要临时演示的场景。8. 常见问题速查与答题思路最后把最容易碰到的故障场景整理成一个速查表后台运维和答辩自检的时候直接对着看。问题现象排查思路定位方案接口返回401检查token是否过期拦截器排除路径配置是否正确先看浏览器Network请求头是否带了token再看后端拦截器排除的路径列表分页查不到数据但SQL注释正常检查PageHelper配置或手写LIMIT参数是否一致打印SQL确认count查询和limit拼接的正确性前端显示时间与数据库不一致排查时区配置与Jackson格式确认jdbc连接url带serverTimezoneAsia/Shanghai批量导入中途失败且无有效提示Excel行数据含有非法字段导入时先做全量校验分批事务提交定位出错行号单独标记反馈接口访问特别慢检查SQL是否有隐式类型转换或未走索引打印慢SQL日志用EXPLAIN分析执行计划每次排查问题无非就是先看配置再看SQL最后看代码层层递进不要一上来就在业务代码里大海捞针。9. 一些掏心窝的总结体会做这类管理系统最深的感触是技术本身真的不是最大的门槛需求分析的完整度、数据模型设计的合理度、细节的周全程度才是项目成败的关键。SpringBootVue这个技术栈足够成熟社区资料也极其丰富遇到问题基本都能搜到答案真正拼的是谁能把业务吃透把边界想清楚。做养老项目还有一个道义上的体验就是当你看到体检记录模块真的能帮助护工快速查到某位老人的血压趋势用药提醒功能让老人按时吃上药你会觉得写代码这件事确实能做出有温度的产品来。这个系统后续如果要扩展我比较看好的方向是接入物联网比如智能床垫的离床监测数据和智能手环的生命体征数据让系统自动生成异常预警再往后走可以对接区域养老服务平台做数据上报。这套代码的基础架构完全撑得住这些扩展。最后说一句不管你拿这套系统是做实操练习还是毕业设计建议花时间把建表SQL从头到尾自己写一遍。理清楚了表之间的关系整个系统的脉络也就通了后面写代码就只是体力活。