ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

养老社区管理系统实战:Spring Boot+Vue前后端分离全解析

养老社区管理系统实战:Spring Boot+Vue前后端分离全解析 2021年7月东北大学软件学院2020级的夏季实训正式开题我们小组拿到的题目是东软颐养社区系统。说实话刚看到这个题的时候我们心里想的是这不就是一个带界面的增删改查吗老人信息、房间床位、护工排班撑死了再加个收费。直到我们真正开始做需求梳理才发现事情远没有想象中简单。一个面向养老社区的管理系统要承接的不只是一张表存人而是一整套围绕老人健康、护理服务、园区运营的复杂业务流。这篇博客是对我们这次实训项目的完整复盘包括题目的需求拆解、技术选型、数据库设计、核心模块实现以及我们在联调和部署阶段真实踩过的坑。如果你也是软件工程专业的在校生正准备做类似的实训或课程设计或者你在做养老、医疗类信息管理系统想看看别人是怎么设计和落地的那这篇内容应该能给你一些参考。1. 实训题目拆解一个养老社区系统背后到底管什么1.1 这个系统不是简单的老人花名册拿到题目第一件事不是写代码是把题目真正看懂。东软颐养社区系统这几个字拆开看每一个词都有信息量。颐养说明它不是医院是养老场景社区说明它不是单栋楼而是园区级别的运营系统说明它要覆盖多角色协作而不是单机工具。我们当时去查了东软在智慧养老方向的资料大致明白了背景这类颐养社区通常有独立房间、公共活动区、医疗护理站老人住在这里护工提供日常照护管理人员负责整体运营。系统要做的就是把线下的这些流程搬到线上让管理员能看清园区状态让护工能高效执行任务让家属能放心。想清楚这些之后我们把系统的核心模块圈定为几个必需项老人档案管理、房间床位管理、员工账号与权限、健康记录管理、护理工单任务、公告活动、简单的数据统计。每个模块背后都有真实的业务诉求而不是为了凑功能硬加进去的。1.2 用角色视角画出功能矩阵需求梳理阶段我们用了最朴素也最有效的方法把系统中的每个角色列出来再逐个问这个角色每天打开系统要干什么。系统管理员维护员工账号、管理楼栋房间和床位、办理老人入住/退住/换房、给护工派发工单、发布园区公告、查看运营数据。护理人员护工查看自己被分配负责的老人列表、录入每日健康巡检数据血压、心率、体温等、接收并完成护理工单、标记用药提醒。家属访客角色查看老人基本情况和健康记录在实训版本里我们做成了给家属开放的部分查询权限但考虑到复杂度最终弱化成公告和健康趋势展示。这三个角色之间的关系其实是一条服务链管理员制定规则并分派任务护工执行服务并记录结果家属查看服务过程。系统所有功能设计都围绕这条链展开谁操作什么、数据从哪来、流到哪去一目了然。1.3 实训周期内的范围取舍四周的实训时间不可能把这些全做完所以我们做了明确的范围切割。支付和财务模块只做成简单的费用记录不做真实支付流程排班功能只做人工排班不做自动排班算法移动端不做独立App但前端页面用响应式布局保证能在手机上正常看。这个取舍过程在实训里非常关键。很多组做到最后没交付问题不在于技术难度而在于一开始什么都想要。我们当时的思路是与其十个模块每个都做到60分不如五个核心模块做到100分保证演示的时候每个功能都能跑通经得起老师追问。2. 技术栈选型为什么前后端分离的方案更适合我们这支队伍2.1 三个备选方案摆在桌上组里5个人技术基础参差不齐有Java学得比较扎实的也有上学期JavaWeb还在补考的。选型的时候我们列了三个方案放在一起对比方案优点缺点适合场景Spring Boot Thymeleaf 服务端渲染上手快一个工程搞定联调成本低前后端代码耦合页面效果一般不好做复杂交互单人开发或两人小团队Spring Boot Vue 2 Element UI 前后端分离页面效果专业前后端并行开发分工清晰需要约定接口联调有成本前端要学框架4-6人团队有两周以上开发时间Python Flask/Django 全家桶开发效率高代码量少组里没人熟悉Python后期维护风险大全员Python基础好的队伍2.2 为什么我们选了Spring Boot Vue 2三个方案里服务端渲染其实最保险因为Java是大家的必修课Thymeleaf模板语法也简单。但我们最后还是选了前后端分离主要是从三个角度考虑的。第一是分工效率。5个人如果全写Java后端的Thymeleaf模板根本没法并行开发一个人改了首页模板另一个人就会冲突。前后端分离之后两个同学负责Vue前端三个同学负责Java后端各自的代码仓库互不干扰。第二是演示效果。实训答辩的评委不会看你的代码结构他们看的是页面。Element UI 的表单、表格、弹窗组件做出来的管理后台比服务端渲染的纯HTML页面专业太多这个优势在最终评分时非常明显。第三是贴近企业真实开发模式。当时2021年找实习的热门方向就是Java后端 Vue前端这套技术在校园招聘里基本是标配。实训不止是为了拿学分简历上能写熟悉前后端分离开发掌握Spring Boot和Vue技术栈也是实打实的收获。2.3 锁定的版本和初始化配置版本问题是我们踩的第一个坑这里直接把我们最终稳定运行的版本列出来后来做类似项目的同学可以直接抄后端JDK 1.8、Spring Boot 2.5.2、MyBatis-Plus 3.4.3、MySQL 5.7、JWT 0.9.1前端Vue 2.6.14、Element UI 2.15.8、Axios 0.21.1、ECharts 5.1.2工具Maven 3.8、Node.js 14.x、IDEA VSCode这里特别提醒一下JWT 0.9.1这个版本它在JDK 1.8下会报ClassNotFoundException: javax.xml.bind.DatatypeConverter需要在pom.xml里手动补一个依赖dependency groupIdjavax.xml.bind/groupId artifactIdjaxb-api/artifactId version2.3.1/version /dependency另外MyBatis-Plus 3.4.x版本的分页插件和旧版本配置方式不一样需要用PaginationInnerInterceptor这个在跑分页查询之前就要配好不然后端接口一直返回全量数据前端分页怎么都做不对。3. 数据库建模一张elder表怎么长出一棵业务树3.1 先梳理核心表再动手建库我们第一版数据库是在一个晚上赶出来的结果第二天就被指导老师问住了你健康记录和护理工单这两张表怎么确定它属于哪一位老人我们才发现很多表之间根本没有外键关联逻辑上完全站不住脚。后来重新梳理了一遍业务最终稳定的表结构是这样的表名关键字段说明sys_userid, username, password, real_name, role, status员工账号role区分admin和nurseelderlyid, name, gender, birth_date, phone, address, emergency_contact, status老人档案主表roomid, building_no, room_no, floor, room_type, status楼栋房间bedid, room_id, bed_no, status房间内的床位elderly_roomid, elderly_id, bed_id, check_in_time, check_out_time入住关系表health_recordid, elderly_id, record_time, high_pressure, low_pressure, heart_rate, blood_sugar, temperature, note健康巡检记录nursing_taskid, elderly_id, assigner_id, executor_id, task_type, content, status, create_time, finish_time护理工单noticeid, title, content, publish_time, publisher_id公告这个表设计里有几个细节是我们在踩坑之后才加上的单独说一下。3.2 老人和床位的关系为什么不能直接存一个room_id最开始的设计里elderly表直接存了room_id和bed_id两个字段看起来简单。但后来发现两个问题一是老人换房的时候要UPDATE原表把之前和历史数据关联全部覆盖掉了二是如果未来要统计这个床位住过哪些老人根本没有记录。所以我们拆了一张elderly_room关系表把入住时间、退住时间单独存。这样老人当前住哪张床、床位历史入住记录、换房记录全部都能查到。这是一个典型的用空间换可追溯性的设计在养老这种需要长期跟踪的场景下非常实用。3.3 健康记录表指标字段必须拆开不能塞JSON这个是我们组内讨论最久的一个设计点。一开始有同学提议health_record表存一个indicators字段把所有健康指标用JSON字符串存进去反正前端展示的时候也是解析完再画图这样表结构简单后面加指标类型也不用改表。这个方案被我们否了原因是数据库里存JSON会导致严重的问题没法按指标做范围查询。比如查一下最近一个月所有血压偏高的老人如果血压值埋在JSON里SQL根本写不出来只能把记录全部查出来在Java内存里过滤数据量一大就废了。所以我们把高压、低压、心率、血糖、体温这些常用指标都拆成了独立字段。折线图查最近30天的数据一条带WHERE high_pressure 140的SQL就能解决。3.4 工单状态用状态机来管理nursing_task表里的status字段我们定义为0待处理 / 1进行中 / 2已完成 / 3已取消。这个状态流转是有方向的待处理可以转进行中也可以转已取消进行中只能转已完成不能跳回待处理。这个约束除了在代码里做校验数据库层面也可以加一个CHECK约束兜底。我们当时为了赶进度没有加数据库约束结果联调的时候就出现了前端连续点击两次按钮、工单状态被直接从待处理改成已完成的情况。如果重来一遍数据库约束一定加上。4. 核心功能落地从登录到工单闭环的完整实现4.1 JWT登录认证与角色权限控制登录模块是整个系统的入口也是我们花时间最多的地方。我们的方案是后端用JWT生成token前端把token存在localStorage里每次请求在Axios拦截器中塞进Header。JWT的工具类核心代码大概是这样的思路public class JwtUtil { private static final String SECRET your-secret-key; public static String generateToken(Integer userId, String username, String role) { return Jwts.builder() .claim(userId, userId) .claim(username, username) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }后端实现了一个HandlerInterceptor在preHandle里统一从Header取token、解析、把userId和role放进Request作用域然后放行。管理员接口路径我们统一设计成/admin/**前缀拦截器里根据role字段判断能不能访问。这里有个我们踩过的坑后面单独讲。前端Vue这边路由守卫里对需要登录的页面做了判断token不存在就跳转到登录页。Axios拦截器里对401状态码做了统一处理token过期就清除本地登录态强制跳回登录页。4.2 MyBatis-Plus 实现老人档案分页与条件检索老人档案的列表页是典型的分页多条件查询。我们使用了MyBatis-Plus的selectPage配合LambdaQueryWrapper代码非常简洁public PageResultElderly pageElderly(Integer pageNum, Integer pageSize, String name, Integer status) { PageElderly page new Page(pageNum, pageSize); LambdaQueryWrapperElderly wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(name), Elderly::getName, name) .eq(status ! null, Elderly::getStatus, status) .orderByDesc(Elderly::getCreateTime); elderlyMapper.selectPage(page, wrapper); return new PageResult(page.getTotal(), page.getRecords()); }前端配合Element UI的el-table和el-pagination一个完整的检索页面很快就能拉起来。值得注意的一个小细节是身份证号的唯一性校验我们除了在业务代码里查重还在数据库字段上加了唯一索引双重保险防止多用户并发提交时插入了重复数据。4.3 健康趋势图ECharts画最近30天血压变化这个模块是我们演示时最出效果的功能。实现思路不复杂后端提供一个接口传入elderlyId返回最近30天的健康记录列表前端用ECharts折线图展示。我们画了两条折线收缩压和舒张压横轴是日期纵轴是mmHg。为了图好看后端返回数据时就按日期排好序前端拿到之后直接用split拆成两个series。另外我们在ECharts的配置里加了markLine标记正常血压范围超出范围的数据点会显示成红色视觉效果非常直观。这个功能在答辩时被老师专门夸过因为它是真正的数据可视化而不是简单列表。其实实现难度不高关键是有这个意识把健康数据用图表呈现比密密麻麻的表格更有信息量。刚跑通健康趋势图的当天我们其实还遇到了一个数据问题测试数据里的高压值是随机的偶尔会出来“高压300”这种明显超标的值调度图上一根针一样扎上去看起来很吓人。后来我们在录入数据的地方加了简单的数值范围校验高压在70-250之间超出就提示输入不合法数据质量才正常。4.4 护理工单的完整闭环派单、接单、完成、统计工单模块是系统业务逻辑最重的地方。管理员的视角是派单选择老人、选择护工、填写任务内容护工的视角是我的任务默认查询executor_id 当前登录用户id的工单点击开始处理把状态从待处理变成进行中处理完点完成再填一段完成备注。我们做了一个小小的统计接口查询当前登录护工今日待办数量、今日已完成数量、平均完成时长。这个接口在护工端首页展示成三个统计卡片实训演示的时候显得业务完整度特别高。具体实现就是基于nursing_task表的executor_id和create_time做分组统计SQL并不复杂。有一点值得提工单完成时间我们用的不是系统当前时间而是在护工点击完成按钮那一瞬间由后端在事务里更新。这个字段看似不起眼但它支撑了后面平均完成时长的计算也方便管理员后续看每个护工的工作量。5. 联调与上线的真实翻车现场5.1 接口字段没对齐前后端对着页面吵了一个小时前后端分离开发最痛的点就是联调。我们联调阶段第一个大坑出现在新增老人接口。后端Java实体类的字段是createTime数据库字段是create_timeMyBatis-Plus默认开启了驼峰转换这没问题但前端同学从Element UI表单里拿到的字段名也是createTime他以为是数据库字段就在提交参数里传了一个create_time。结果后端接收到的实体类里createTime为null插入数据库的时候直接报错字段create_time cannot be null。当时前后端各执一词前端说我明明传了create_time后端说我要的是createTime排查了快一个小时才发现是命名规范的问题。从那之后我们立了个规矩联调之前先过一遍接口文档字段名以Java实体类为准前端不自己脑补字段名。后来我们把Swagger接上了直接在页面上看每个接口的请求参数示例这种问题再没出现过。5.2 权限漏洞护工账号直接调用管理员接口这个是实训过程中最惊险的一个bug发生在答辩前第三天。我们测试的时候偶然发现用护工账号登录之后直接在后端接口地址里输入/admin/user/list居然能查到全部员工列表。原因很弱智拦截器里只做了token是否存在、是否过期的校验根本没判断当前用户的角色。也就是说只要是个登录用户管你是管理员还是护工都能访问所有接口。这就是典型的只做了登录验证没做授权验证。修复很快在拦截器的preHandle里增加角色判断requestURI以/admin/开头时解析token里的role字段必须是admin才能放行。但我们复盘的时候发现之所以会犯这个错是因为我们一开始安全设计的重心全在怎么生成token怎么防止token伪造上反而忽略了最基础的越权问题。这给我们上了一课安全不只是加密算法更是每一个接口上的访问控制。5.3 部署到服务器中文乱码、端口占用、后台启动答辩前我们需要把系统部署到云服务器上。买了最低配的2核4G云主机装好MySQL、JDK、Nginx、Node环境一个坑接一个坑。第一个坑是MySQL建库没有指定字符集导致页面上录入的中文全部变成问号。建库时一定要指定编码CREATE DATABASE yanglao DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第二个坑是Nginx默认监听80端口后端Spring Boot默认8080端口。服务器安全组只放行了80端口前端能打开但请求转发到8080时被安全组拦了。解决办法是Nginx配置反向代理把所有/api/开头的请求都转发到服务器的8080端口。这既解决了端口问题也顺便做了前后端域名统一前端请求不需要写跨域配置。第三个坑是jar包启动方式。我们最开始用java -jar前台启动一关SSH窗口服务就断了。后来改用nohup放后台nohup java -jar yanglao-system.jar app.log 21 这个命令把日志输出到app.log文件关掉终端服务也不会断。排查问题时直接tail -f app.log看报错信息比在IDEA控制台里看方便得多。6. 实训结束后我的三个复盘认知6.1 需求分析花的时间越久后面返工越少我们第一版数据库当天晚上就建完了后来两周里改了三次表结构。每一次改动都牵动后端实体类、前端表单、接口文档一起变成本极高。后来才明白建表之前应该先把业务流程图和数据流图画清楚确认每个数据从哪来、到哪去、谁修改、谁删除。这些工作看起来不产生代码但能省下后面无数改bug的时间。6.2 接口文档是团队协作里的最大公约数前后端分离的团队里接口文档不仅仅是记录了接口长什么样更是前后端双方对业务理解的统一约定。我们一开始用Markdown写接口文档后来换成了Swagger前端同学可以直接在页面上看参数示例和响应结果。哪怕时间紧张接口文档也绝对不能省这是我们在联调上吃过最大亏的地方。6.3 一个完整的闭环比十个零散页面更有说服力实训答辩我们会看其他组的展示发现有的组做了十几个页面但每个模块之间没有任何数据联动明显是各写各的。而我们只做了五个核心模块但它们是串起来的管理员录入老人档案、分配床位、创建健康记录模板、派发护理工单护工登录看到自己负责的老人和任务完成任务后状态回写管理员在统计页能看到完成率。这个完整的数据闭环让我们在答辩时讲起来非常有底气老师也觉得这是一个整体系统而不是一堆页面的拼凑。最后再说一个实用的小经验答辩前一天我们组专门把整套流程从登录开始走了一遍拿了一份真实的测试数据包括一位老人完整的入住、健康记录、工单记录然后录屏保留。演示的时候即使现场网络出问题也可以用录屏兜底。这个习惯后来我在工作里也一直保留每次都帮我避免了在关键场合翻车的尴尬。
返回列表