资讯动态

护工管理系统开发实战:Vue+Node.js从需求到部署完整指南

发布时间:2026/10/14 23:26:44 来源:尧图企业网站定制
做护工管理系统的需求最早是某个社区养老服务机构的负责人找到我。当时他们管着二十多个护工、服务着七八十位老人排班靠一张A3纸贴在墙上谁请假了就用红笔涂改服务记录的归档全靠护工随手写在纸质工单上。老人家属想知道今天的护理情况只能打电话问护工。他们负责人说了一句话让我印象很深“我们不是没有在做管理而是管理动作本身就成了负担。”这句话基本就是整个项目的起点也直接决定了这套系统的定位——不是做一套看起来很“全”的ERP而是做一套能让管理员、护工、家属三方都少操心的护工管理便捷服务系统。技术栈选了Vue和Node.js不是因为这些技术最“时髦”而是因为它们最适合这个体量和业务类型的项目。这篇文章我想把从需求梳理、技术选型、数据建模到模块落地、上线排错的全过程都写出来。尤其是那些不跑一遍根本发现不了的坑这才是做这类业务系统真正值钱的东西。如果看完你也要做类似的管理系统或者正在为“系统做出来了但没人用”发愁那这篇文章多少能帮你省几个月的试错时间。1. 护工管理的特殊性这不是一个普通的“人员管理”系统1.1 看似管人实际管的是服务过程很多人第一次听“护工管理系统”会觉得这就是一个花名册加排班表。真做进去才会发现完全不是那么回事。普通的人员管理系统管的是“人”的静态属性姓名、部门、入职时间、考勤记录。护工管理系统管的是“服务”这个动态过程谁在几点上门、用户当前处于什么状态、这次服务做了哪些项目、有没有超出约定时长、服务结束后用户的异常情况要不要上报、费用按什么标准结算。一个护工一天可能会服务2到3位老人每位老人的服务时长、项目、费用标准都可能不同。这就意味着系统里的核心对象不是“护工”也不是“老人”而是“服务工单”。护工、老人、管理员的所有操作最终都是为了把一个工单从“待派单”推进到“已完成”的状态。1.2 传统管理方式的断层是痛点的来源我在前期调研时认真看了他们的作业流程可以用一句话概括信息散布在各个不可同步的载体上。排班表在纸上改了不会通知到每个人护工的服务记录在纸质工单上管理员要等晚上收工后一张一张录入Excel家属的询问在微信里护工没空回管理员也不一定知道具体情况费用结算靠月底翻单据经常出现“护工说自己做了10次记录里只有8次”这类扯皮。这些问题的共性不是“某一环节太落后”而是没有任何一条贯穿全程的信息流。做这个系统的核心目标也不是“数字化”而是把信息流拉通。1.3 三方用户画像与诉求边界这个系统有三类使用角色诉求差异非常大是后续设计权限和界面的关键。管理员最关心排班效率、工单完成情况、费用结算准确度。他们希望系统能减少对账工作量最好一键导出报表。护工手机端操作居多最在意的是“别给我添麻烦”。他们要快速看到今天跑哪几家、服务什么内容签到签退要简单直接最好一步完成。家属老人不太愿意学复杂操作他们会看服务完成后的记录、时长、评价入口以及发现异常时能不能快速反馈。这三类角色对“便捷”的定义完全不同。系统设计里最大的权衡点也是在这里管理员要的是“信息全面”护工要的是“操作最少”家属要的是“看得明白”。后面在页面和接口设计上我一直在围绕这三个诉求做取舍。2. 技术方案取舍为什么这个项目选择了Vue加Node.js2.1 前端用Vue的理由不是因为“生态繁荣”这种空话Vue在这个项目里最大的价值是渐进式改造能力和中小团队的协作效率。业务系统的页面特点是列表多、表单多、状态联动多。护工端的首页要展示“今日排班列表服务进度消息提醒”管理员端的看板要同时容纳“工单统计待办审批异常预警”。用Vue的组件化方式组织这些页面非常顺手路由配置加组件拆分的模式一个前端开发可以轻松维护几十个页面而不失控。另一个理由是Vue对“不专业的用户”很友好。系统上线后管理员用电脑浏览器访问护工大概率用的是带壳浏览器或微信内置浏览器前端兼容性方面Vue做了很多底层工作基本不用担心白屏问题。2.2 后端选择Node.js的真实考量选Node.js不是因为它性能比Java或Go更好——恰恰相反这种业务系统根本没有高并发压力性能从来不是瓶颈。真正的理由是三点前后端语言统一。这个系统规模不大不太可能专门配一个Java团队。全栈用JavaScript/TypeScript一个人或者一个小团队可以从接口一路写到组件减少沟通损耗。快速迭代友好。护工管理系统的需求在运行初期会有很多调整比如排班的展示形式、结算规则的计算逻辑。Node.js可以热重启改完立即可用开发效率高。生态匹配。Excel导入导出、定时任务、微信通知对接、文件上传这些在npm生态里都有成熟方案拿来就能用不必重新造轮子。我选框架的时候没有用NestJS这类重框架而是用了Express并自己做好了分层。原因是在这个量级的项目里NestJS的依赖注入和装饰器体系会增加团队学习成本而Express自由度高逻辑足够直白出了问题也容易排查。2.3 数据库和中间件的基础选型数据库用的MySQL原因非常简单这类业务数据本质上是强关系型的排班、工单、结算、评价之间都有复杂的关联查询。MongoDB虽然建模灵活但在按月结算、按人统计这类聚合查询上写起来会让人头大。另外引入了Redis做两层用途一是存登录态和权限标识二是做排班锁。同时排班时如果两个人改到同一个护工身上Redis的分布式锁可以避免请求覆盖问题。项目早期没有用Redis是等到真实发生了一次“排班互相覆盖”的事故后才加上的这个教训后面会详细讲。3. 数据模型设计先定工单状态机再谈功能模块3.1 核心表结构与关系在设计表结构时我没有直接套用“用户表角色表权限表”这种通用模版而是围绕服务工单这个核心对象来组织。主要的数据表如下表名用途关键字段caregiver护工档案姓名、手机号、资质等级、状态elder被服务老人档案姓名、家属手机号、住址、照护等级care_order服务工单护工ID、老人ID、排班日期、时段、状态care_visit服务打卡记录工单ID、签到时间、签退时间、服务时长care_item服务项目明细工单ID、项目名、时长、金额settlement结算记录护工ID、月份、工单数、总金额evaluation服务评价工单ID、评分、内容、异常标记这套表的关系其实就是一个立方体护工、老人、日期时间三个维度确定唯一一条care_order记录。所有统计报表几乎都是从care_order出发做的聚合。3.2 工单状态机的设计是全部业务逻辑的中枢care_order的status字段是整个系统里最重要的字段。我定义了六个状态确保每个状态之间的流转路径清晰明确待派单 - 待服务 - 服务中 - 待确认 - 已完成 - 已结算其中有两个比较容易被忽略的状态节点待确认护工点击“完成服务”后工单不会立刻变为已完成而是进入待确认。管理员需要对服务时长、项目明细做一次核对。这是为了防止护工误操作或虚报时长。已结算和付款动作绑定。只有管理员在结算页面确认后工单才进入已结算状态进入结算报表。状态流转图可以用代码方式表达看起来像一个标准状态机但真正复杂的是各种异常路径——比如护工临时请假怎么办老人临时不在家怎么办服务到一半家属要求提前结束怎么处理这些问题不能全都塞进状态机里否则会变得非常臃肿。我的处理方式是状态机管正常路径特批、取消、改期都通过“操作记录表”记录同时把工单状态重置为流程初始状态或终止状态管理员拥有强制改状态的权限但每一次强制操作都会记录操作人和原因。这套机制既保证了流程的规范性又给业务留了弹性的口子。3.3 结算模块的建模逻辑结算的需求是按月统计每个护工完成工单数、累计服务时长、按照服务项目单价计算金额再扣除请假扣款、好评奖励、工具损耗费等。为了不让结算逻辑后期变成一坨乱麻我将“费用结果”和“费用来源”分开存储。settlement保存每个护工当月的最终金额settlement_detail保存每一笔费用的来源明细。这样即使对账时有分歧也能从明细逐条找到原始工单。更关键的一点是——结算的幂等性。财务人员很可能多次点击“生成结算单”按钮因此生成结算单的接口必须做防重处理。实现方式很简单provider_id和month字段加唯一索引重复提交时直接返回已存在的结果。这个设计虽然不起眼但在实际使用中避免了大量脏数据。4. 核心功能模块落地页面与接口怎么配合4.1 护工端一个“今日工作台”解决所有操作护工端的原则是“打开即见今天要干的活”不需要他们去翻排班列表。初始化接口大概长这样// routes/caregiver.js router.get(/workbench, authMiddleware([caregiver]), async (req, res) { const today dayjs().format(YYYY-MM-DD); const caregiverId req.user.userId; const orders await db.query( SELECT co.id, co.time_slot, e.name AS elder_name, e.address, co.status, cv.checked_in_at, cv.checked_out_at FROM care_order co JOIN elder e ON co.elder_id e.id LEFT JOIN care_visit cv ON cv.order_id co.id WHERE co.caregiver_id ? AND co.order_date ? ORDER BY co.time_slot ASC, [caregiverId, today] ); res.json({ data: orders }); });前端页面拿到这个数组后按时间段分组渲染成卡片列表。护工看到“张奶奶 09:00-10:30”的卡片点进去就能看到该老人当天的照护注意事项点击“签到”按钮即进入服务中状态。技术上有一个细节签到按钮的状态切换需要和后端时间校准不能信任手机本地时间。我在签到接口里统一返回服务器时间前端展示的服务时长都以服务器时间为准避免手机时间不准导致的计时纠纷。4.2 管理端排班看板是核心中的核心管理端的排班看板本质上是一个按时间轴和护工维度交叉的矩阵表格。按天列出老人的服务需求按行列出护工每个交叉单元格表示某护工在某个时段是否已安排。排班的核心难度是冲突检测。一个护工不能同时在两个地方服务一个老人同一个时段也不能被安排两个护工。后端在插入或更新排班时需要做一次重叠校验// services/scheduleService.js async function checkConflict(caregiverId, elderId, date, startTime, endTime, excludeOrderId null) { // 同一个护工在重叠时间段的排班 const caregiverConflict await db.query( SELECT id FROM care_order WHERE caregiver_id ? AND order_date ? AND status ! cancelled AND (start_time ? AND end_time ?) AND (? IS NULL OR id ! ?), [caregiverId, date, endTime, startTime, excludeOrderId, excludeOrderId] ); // 同一位老人在重叠时间段的排班 const elderConflict await db.query( SELECT id FROM care_order WHERE elder_id ? AND order_date ? AND status ! cancelled AND (start_time ? AND end_time ?) AND (? IS NULL OR id ! ?), [elderId, date, endTime, startTime, excludeOrderId, excludeOrderId] ); return { caregiverConflict: caregiverConflict.length 0, elderConflict: elderConflict.length 0 }; }这个校验的SQL写成start_time ? AND end_time ?是为了处理“前一个服务刚结束后一个服务马上开始”的边界情况——只要两者时间相等就不算冲突衔接上是允许的。4.3 家属端不需要注册账号只需要验证码家属端做起来最麻烦的是“如何让家属愿意用”。很多家属对这类系统的期望值是“顺手看看”如果让他们下载APP或者注册复杂账号基本就会被弃用。我的方案是**每位老人绑定一个或多个家属手机号。家属用手机号加验证码登录进入后只能看到与该老人关联的工单数据和评价入口。**管理员在后台录入老人档案时顺手录入家属手机号不需要家属自己激活。这看起来是一个很简单的设计但实际效果非常好。上线后家属的周活跃率一直维持在80%以上因为打开成本足够低。后面做消息推送时也是复用这套手机号体系。4.4 通知模块短信、微信模板消息、站内信三级触达护工端和管理端的使用场景不同通知策略也要区分排班变更推送给护工和管理员通过短信加微信服务号模板消息双通道避免漏看。工单状态更新在护工App内用站内信提醒即可没必要触发短信。服务完成提醒发短信给家属。短信内容要简洁包含老人姓名、服务时间、服务项目不用跳转链接避免被当成垃圾短信。短信渠道我做过对比最终选了某三家云服务商中的一家稳定性好而且便宜。但要注意对接短信服务时短信签名需要提前备案而且第一个月的发送量可能会有条数限制这些要提前走流程否则上线当天容易卡在通知环节。5. 权限与状态管理系统翻车概率最高的模块5.1 JWT鉴权与三端权限控制前后端分离的项目最常用的鉴权方案是JWT。在这系统里JWT外还叠加了一层“端侧标识”。我在生成Token时把role字段写进Token内部。接口中间件统一验证三件事Token是否有效签名和过期时间角色是否匹配当前接口的要求数据范围是否允许比如护工只能查自己的工单管理员可以查所有这层逻辑虽然简单但有一个很容易疏忽的点普通Token和刷新Token的过期策略要分开。访问用的Token我设了2小时过期刷新Token设了7天。护工用手机端时经常是连续好几天不登录如果不做自动刷新他们打开系统就会看到登录页体验很差。实现刷新接口时前端需要做请求拦截器。这里给出Vue侧的关键代码// utils/request.js instance.interceptors.response.use( (response) response.data, async (error) { const status error.response error.response.status; if (status 401 !error.config.__isRetry) { const refreshToken localStorage.getItem(refreshToken); if (refreshToken) { try { const { data } await axios.post(/api/auth/refresh, { refreshToken }); localStorage.setItem(token, data.token); error.config.__isRetry true; error.config.headers[Authorization] Bearer data.token; return instance(error.config); } catch (e) { router.push(/login); } } } return Promise.reject(error); } );5.2 Vuex该放什么这个项目里我只放了三种东西前端状态管理我用的是Vuex但只放了一个很小的核心集合没有把所有接口数据都塞进去。实际只放了三类数据当前登录用户信息name、role、avatar系统全局配置服务类型字典、时段字典管理端看板的筛选条件缓存为什么不多放业务系统里大量数据是“页面私有”的比如工单详情、排班表、结算明细。把这些数据放进Vuex会让store变得极其臃肿并产生大量无效的响应式监听不仅没有提升性能反而会带来“数据不同步”的隐形问题。页面私有数据直接在组件里管理配合keep-alive缓存就够了。这也是我想提醒的一点**状态管理工具是用来解决共享状态问题的不是用来解决页面通信问题的。**如果按后一种思路来用系统迟早会被自己复杂死。5.3 防重复提交护工手速快是真实存在的威胁在实际上线后我发现一个特别实际的问题护工在服务结束时点击“完成服务”按钮因为网络稍有延迟他们往往会连续点好几下。如果接口没有做防重处理就会生成多条完成记录。我在后端做的处理非常朴素但有效——在接口入口处加上请求唯一性校验。// middlewares/idempotent.js module.exports function idempotent(req, res, next) { const idempotentKey req.headers[x-idempotent-key] || ${req.user.userId}_${req.path}_${req.body.orderId}; const key idempotent:${idempotentKey}; redis.set(key, 1, EX, 10, NX, (err, result) { if (err) return next(err); // 如果返回结果不是 OK说明短时间内同一个请求已被处理过了 if (result ! OK) { return res.json({ code: 0, message: 请求正在处理中请勿重复提交 }); } next(); }); };这种方案虽然简单但在“护工连点两次”的场景下非常有效。前端的按钮也做了loading禁用双保险之后再也没有出现过重复工单的事故。6. 上线前后的真实排错记录以为没问题的地方全是问题6.1 时区问题和“消失的一小时”上线后的第二周有管理员反馈某天下班统计时发现有位护工的服务时长总共少了1小时结算金额也随之少了。排查下来原因出在服务器时区与本地时区的偏差上。前期开发时用的是new Date()取本地时间测试环境的服务器时区就是北京时间完全没问题。上线后用了云服务器默认时区是UTC导致管理员在页面上看到的人工排班时间是“09:00”但后端存进数据库时UTC化变成了“01:00”查询当天工单时用了数据库时间查询范围和显示范围不一致计算服务时长时两个时间点之间的差值看似是“格子差”实际被系统时区偏移干扰。解决方案是在后端启动时设置时区同时所有时间字段统一使用ISO格式字符串传给前端由前端通过dayjs转换并显示本地时间。// app.js process.env.TZ Asia/Shanghai;这个改动看着轻描淡写但在真实项目里花了将近一个晚上才定位到。做业务系统的人一定要记住时区问题不只在跨国应用中存在云服务器的默认时区就足以挖坑。6.2 “排班冲突”的灵异事件数据库里没有重复页面上却有排班上线的第三周管理员反馈S护工在周三上午10点到11点同时出现在两个小区的排班中。但是查询数据库care_order里并没有重叠的记录。排查过程很有意思。先怀疑是缓存问题查了Redis也没有缓存排班数据。后来逐条看了前端请求的参数发现管理员在编辑排班时前端把旧的工时长度和新的开始时间一起提交了。比如原有排班是“09:00-10:00”管理员把开始时间改成10点但忘了修改结束时间前端提交的字段是start_time10:00, end_time10:00。这时冲突检测发现“10:00-10:00”和“10:00-11:00”看起来并不重叠但渲染到前端时因为生成排班块的逻辑是按“开始时间默认时长”来绘制的看起来就变成了10:00-11:00于是视觉上产生了冲突。根因是前端提交的数据和后端用于校验的数据来源不一致。修复方式是后端在做冲突检测前对end_time做一次标准化校验如果结束时间早于或等于开始时间直接拒绝请求并提示。这个教训说明接口不只要做权限校验还要做基础的数据合法性处理。6.3 图片上传超时的根因不是服务端性能问题系统里有一块“老人档案”功能家属可以上传老人的照片。上线后频繁出现上传超时一开始怀疑是Nginx的client_max_body_size限制调大后还是超时。最终定位到护工和家属用的手机拍照默认是3000万像素照片体积普遍在6MB以上。上传时前端没有做压缩处理后端虽然用Multer处理了接收但受限于云服务器的带宽和PHP/Nginx的同步处理能力单个文件超时就成了家常便饭。解决方案是在前端做图片压缩// utils/compressImage.js async function compressImage(file, maxSizeKB 500) { const dataUrl await readFileAsDataURL(file); const img await loadImage(dataUrl); const canvas document.createElement(canvas); const maxWidth 1280; const ratio Math.min(1, maxWidth / img.width); canvas.width img.width * ratio; canvas.height img.height * ratio; const ctx canvas.getContext(2d); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const compressedDataUrl canvas.toDataURL(image/jpeg, 0.7); // 如果压缩后还超过 500KB可以继续降低质量参数 return dataUrlToBlob(compressedDataUrl); }压缩后图片从6MB降到300KB左右上传速度快了十几倍。这是典型的“做业务系统最容易忽视的性能优化点”它不涉及复杂的架构改造就是一个认知问题真实用户设备产生的数据量和开发者本机上的测试数据完全不是一个量级。6.4 批量导入排班时的“事务黑洞”系统支持管理员用Excel批量导入排班。第一次用真实数据导入时500条排班记录导入了大概40秒页面直接卡死。原因是导入接口是纯同步实现的所有数据插入都在一个事务里事务越长锁持有的时间越长前端就一直等着接口返回。后来我重构了导入流程前端上传Excel后后端先保存文件解析出结构化数据存到import_task表返回“任务已接收”响应前端轮询任务状态后端逐条插入数据每50条提交一次事务导入完成后写一份结果文件标记哪些行导入失败及原因。这样用户体验从“一直转圈”变成了“任务进行中”护工管理员还能看到失败行并重新处理。7. 部署与回访从“能跑”到“大家愿意用”的距离7.1 生产环境的部署细节部署用了经典的Nginx反向代理加PM2进程守护。Nginx负责托静态文件、转发API请求以及处理HTTPS证书。有一点值得提醒Node.js服务要开cluster模式。这个系统的并发量很低但PM2 cluster模式可以保证某个进程崩溃后另一个进程能立即接管不至于让服务中断太久。pm2 start ecosystem.config.js # ecosystem.config.js 示例 module.exports { apps: [{ name: care-system, script: ./src/app.js, instances: 2, exec_mode: cluster, env: { NODE_ENV: production, PORT: 3000 } }] };7.2 监控和日志接上一个错误上报服务就够了很多小团队会觉得“监控是大公司才需要的东西”直到线上出现过一次无人察觉的长时间宕机才意识到监控的价值。我给这个项目做了最轻量级的监控组合请求日志morgan输出每个接口的耗时和状态码全局错误上报在Express错误处理中间件里接入了前端错误上报系统将后端异常自动推送到群聊通知健康检查接口/health返回进程状态和数据库连接状态用定时任务每分钟请求一次失败则报警。实际用下来效果很好。有一次数据库连接池打满就是因为没有及时监控到异常请求导致雪崩。接入监控后这类问题基本可以在影响扩大前就被发现。7.3 上线三个月后回访中发现的使用习惯回访过程中有几个数据值得分享护工端的日活使用率达到了100%所有护工都愿意用手机签到因为“查地址、看注意事项确实方便”家属端的月平均打开次数是6次左右主要集中在服务完成后的记录查看管理员的报表演进功能使用频率最高他们不再需要手动录Excel了。但也有一个“失败”案例系统做了“服务评价”功能设计了精美的评分卡片和倾向性标签结果上线后使用率几乎为零。后来访谈才发现家属普遍不好意思在护工刚走就打分而且担心打分会影响护工下次服务态度。这个反思很重要——不是所有“看起来有价值”的功能都适合当下场景。过高的交互成本会拖垮用户的信任感。最终我们把评价功能改成“异常反馈”按钮仅在服务确实有问题时才需要填表单家属反而更愿意用了。8. 项目可继续延伸的方向这个系统目前只能说解决了“从无到有”的问题但距离“从有到优”其实还有不少路可以走。比较务实的方向有三个数据看板和分析目前只是每天做基础的工时统计如果加入护工工作量饱和度分析、服务评分趋势预测、老人照护等级变化跟踪管理价值还能提升一个台阶。自动排班算法现在的排班都是人工操作加冲突检测效率已经上来了。但如果接一个“按需自动推荐排班”的算法比如结合护工技能等级、路途距离、老人需求时段做最优化推荐能进一步降低管理成本。服务标准化与培训模块把护工服务流程标准化再加上护理知识库和在线测评让护工从“接单干活”变成“持续成长”这对服务质量提升的意义之大可能比系统本身还大。不过这些都建立在“基础流程已经跑顺”的前提下。做这类系统最忌讳一上来就堆功能把主流程做顺、把用户习惯养好之后再慢慢扩展才是健康的节奏。这次项目实施下来我个人最大的感受是——技术上的挑战从来不是最难的最难的是让业务里的每个人都能在系统里找到属于自己的效率。护工觉得手机签到方便管理员觉得报表好用家属觉得消息透明系统自然就会被用起来。反过来哪怕技术架构再优雅如果用户每天都要跟系统斗智斗勇那它迟早会被冷落。所以每一次功能迭代前我都会先问自己一个问题这个功能到底是在创造价值还是在制造新的负担。这个问题同样适合每一个将要开发业务系统的朋友。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑