拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于SpringBoot+Vue3的敬老院管理系统开发实战

接手这套敬老院管理系统之前我其实犹豫过一阵子。市面上类似的项目模板不少但大多是“能跑就行”的半成品数据库设计粗糙、权限没闭环、前端页面能看不能用。这次从零搭了一套 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 的前后端分离项目跑通之后整体的稳定性和可维护性都在预期之上。这篇就把整个系统的设计思路、关键代码、数据库落地细节和实测踩坑过程完整记录下来既作为项目复盘也方便想做养老机构管理系统的同学参考。系统本身面向的是养老机构的信息化管理场景老人档案、入住退住、床位分配、护理记录、健康档案、费用缴纳、家属关联、统计报表。技术层面覆盖了 Java Web 全栈开发中最常见也最核心的一整套组合对于想巩固 SpringBoot 后端能力、熟悉 Vue3 组件化和后台管理系统开发的人来说这套源码加上配套文档完全可以直接拿来做二次开发或者课程设计。1. 从业务需求到技术选型为什么是 SpringBoot2 Vue3 这套组合1.1 敬老院业务场景的真实痛点敬老院管理的日常工作远比想象中琐碎。纸质档案分散在各个楼层老人基本信息、紧急联系人、既往病史可能登记在好几张表上床位状态靠口头交接班哪个房间空着、哪个床位住着失能老人信息经常滞后护理记录、用药提醒、缴费账单又各自为政。做这套系统的第一件事不是写代码而是把这些线下流程拆清楚老人全生命周期管理入院评估-入住-日常照护-退住、员工排班与护理执行、费用结算、家属沟通。这几个主流程梳理明白之后功能模块和表结构就顺理成章了。1.2 技术栈选型的核心考量选 SpringBoot2 而不是 SpringBoot3主要是考虑生态兼容性和稳定迭代。SpringBoot2 的集成方案最成熟网上遇到的坑基本都有解决方案不容易卡住SpringBoot3 虽然新但配套的框架版本还在快速变动期对于业务系统来说稳定优先。Vue3 这边我直接采用 Vite Vue3 Element Plus 的组合没用 Vue2。Vue3 的组合式 API 在编写业务逻辑时比选项式 API 清晰很多特别是老人档案这种字段多、表单联动复杂的页面setup 语法糖配合 reactive 和 computed状态流转一目了然。Element Plus 组件库在表格、表单、弹窗这些后台管理高频场景上非常成熟开箱即用。MyBatis-Plus 和 JPA 的选择我的判断很简单这个项目的查询场景以多条件组合检索为主老人姓名、床号、入住时间范围、护理等级MyBatis-Plus 的 LambdaQueryWrapper 能直接在 Java 代码里拼条件不用写一堆动态 SQL 标签同时又保留了手写 SQL 的灵活性遇到复杂报表查询可以直接在 XML 里写原生 SQL。项目里两套能力都用上了这一点在后端实战章节展开。MySQL8.0 则是当前 Java Web 项目最稳妥的数据库版本JSON 类型、窗口函数、更好的索引下推在报表统计和健康数据存储上有实际收益。1.3 总体架构和项目结构总览系统采用标准的前后端分离结构。后端 SpringBoot 应用单独部署提供 RESTful API前端 Vue3 工程通过 Vite 构建后由 Nginx 托管。前后端之间通过 JWT 令牌维持登录态接口统一返回 Result 包装对象。后端根目录结构如下nursing-home/ ├── sql/ # 数据库初始化脚本 ├── docs/ # 设计文档、接口文档 ├── backend/ │ ├── src/main/java/com/nursing/ │ │ ├── common/ # 通用返回对象、异常处理、工具类 │ │ ├── config/ # 拦截器、跨域等配置 │ │ ├── controller/ # 接口层 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis-Plus Mapper 接口 │ │ ├── entity/ # 数据库实体 │ │ ├── dto/ # 入参出参对象 │ └── src/main/resources/ │ ├── mapper/ # XML 文件 │ └── application.yml └── frontend/ └── src/ ├── api/ # 接口请求封装 ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 └── views/ # 页面组件这种结构完全按照业务模块而不是技术类型来分包刚开始可能觉得有些别扭但功能模块一多优势就出来了老人相关的 controller、service、mapper 你不需要跨目录找改动范围一眼就能圈定。2. 核心功能拆解与数据库设计表、字段、状态机2.1 功能模块地图系统按业务域分成六大模块这也是文档里的功能结构主线模块核心功能关键操作老人档案基本信息、入住登记新增/编辑/查看、子女关联住宿管理房间床位、入住退住床位分配、换房、退住护理管理护理计划、护理记录按护理等级生成任务健康档案健康指标、体检记录新增查体、趋势展示费用管理账单生成、缴费登记按月生成账单、收款系统管理用户、角色、权限菜单权限分配2.2 关键数据表设计数据库设计是这个项目投入精力最多的地方。这里挑几张核心表说明设计思路老人基本信息表 elder_infoCREATE TABLE elder_info ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, name varchar(50) NOT NULL COMMENT 姓名, gender tinyint DEFAULT NULL COMMENT 性别 1男 2女, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, birth_date date DEFAULT NULL COMMENT 出生日期, health_level tinyint DEFAULT NULL COMMENT 护理等级 1自理 2半护 3全护, room_id bigint DEFAULT NULL COMMENT 当前房间ID, bed_id bigint DEFAULT NULL COMMENT 当前床位ID, status tinyint NOT NULL DEFAULT 1 COMMENT 状态 1在住 2退住 3待入住, emergency_contact varchar(50) DEFAULT NULL COMMENT 紧急联系人, emergency_phone varchar(20) DEFAULT NULL COMMENT 紧急联系电话, create_time datetime DEFAULT NULL COMMENT 创建时间, update_time datetime DEFAULT NULL COMMENT 更新时间, deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除, PRIMARY KEY (id), KEY idx_name_status (name,status), KEY idx_room_bed (room_id,bed_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT老人基本信息表;几个关键设计决策一是health_level护理等级单独拎出来它直接影响后续护理计划生成和收费标准二是room_id和bed_id拆开存因为存在老人换床但同一个房间不变的情况也方便后面做床位总览时直接按这两个字段联动查询三是全局加了deleted逻辑删除字段老人数据是核心资产物理删除风险太大。费用账单表 fee_bill的表设计则体现了状态机的思想CREATE TABLE fee_bill ( id bigint NOT NULL AUTO_INCREMENT, elder_id bigint NOT NULL COMMENT 关联老人, bill_month varchar(7) NOT NULL COMMENT 账单月份 2025-03, item_type tinyint NOT NULL COMMENT 费用类型 1床位费 2护理费 3伙食费 4医疗费, amount decimal(10,2) NOT NULL COMMENT 金额, status tinyint NOT NULL DEFAULT 0 COMMENT 账单状态 0待支付 1已支付 2已作废, pay_time datetime DEFAULT NULL COMMENT 支付时间, operator_id bigint DEFAULT NULL COMMENT 操作人, PRIMARY KEY (id), UNIQUE KEY uniq_elder_month_type (elder_id,bill_month,item_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT费用账单表;status字段定义了待支付、已支付、已作废三个状态后续业务上继续扩展不需要改表结构。唯一的联合索引uniq_elder_month_type避免了同一个老人同一月份同一费用类型重复生成账单这在财务数据上是硬约束必须靠数据库兜底而不是业务逻辑判断。2.3 业务状态机入住-退住流程入住和退住是两个涉及多张表联动的操作不能在 service 里随便 update 一两张表就完事。入住时需要在elder_info中写入老人信息和床位信息、把床位表bed_info的状态改成已住、插入一条入住记录到check_in_record、按当月剩余天数生成首月费用账单。这是一个典型的事务操作不是简单的增删改。退住流程正好相反校验是否有未结清账单如有则先生成结算账单并拦截退住确认结清后更新老人状态为退住、释放床位、写入退住记录和退住时间。所以设计文档里一定要把这套流转图画清楚后端实现时用Transactional把整个流程包起来任何一个环节失败都要全部回滚。我看到很多类似的系统把状态散落在各种 if 里导致后期维护极其痛苦。我的经验是每个涉及多表状态变更的业务动作单独拆成一个 service 方法并且注释里写明前置条件和后置效果。代码可读性和项目文档的维护性会好很多。3. 后端落地分层架构、登录鉴权与 MyBatis-Plus 实战3.1 分层设计与包结构后端采用 controller - service - mapper 三层架构实体对象和传输对象分离。老项目经常把 entity 直接当出参返回给前端这样做的问题在于加了逻辑删除字段或者内部状态字段容易把不该暴露的数据漏出去。所以我在项目里强制约定entity和数据库表一一对应内部使用。dto接收前端参数显式声明字段做参数校验。VO返回给前端的数据可以自由组合多表字段。例如老人列表页需要一个包含房间号、床位号、当前账单总额的视图你不可能在 elder_info 那张表里直接查出来必须在 service 层组合查询后封装到ElderVO返回。如果没有 VO 层就会逐步演变成“返回整张表给前端”的坏味道。3.2 登录鉴权JWT 拦截器登录这块我没有引入 Spring Security 全家桶而是用 JWT HandlerInterceptor 实现了一个轻量级方案。安全性和可扩展性对于这个规模的项目完全够用代码量却少一个量级。核心逻辑分三块登录接口生成 tokenPostMapping(/login) public ResultString login(RequestBody Valid LoginDTO dto) { SysUser user userService.lambdaQuery() .eq(SysUser::getUsername, dto.getUsername()) .one(); if (user null || !PasswordUtil.matches(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } UserToken userToken new UserToken(user.getId(), user.getUsername(), List.of(user.getRoleIds())); String token JwtUtil.createToken(userToken); return Result.success(token); }拦截器校验Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (StrUtil.isBlank(token) || !JwtUtil.verify(token)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } UserToken userToken JwtUtil.parseToken(token); UserContext.set(userToken); return true; }UserContext 上下文用 ThreadLocal 保存当前登录用户后续查询创建人、操作人时直接取不需要每次从请求参数里带。记得在afterCompletion里移除不然请求线程复用时数据会串这个坑我在项目联调阶段实实在在踩过后面排错章节细说。还有一个细节密码存储必须加盐哈希不能用明文也不能用简单的 MD5。我封装了一个基于 BCrypt 的PasswordUtil这是行业标准方案。3.3 MyBatis-Plus 的典型用法与 XML 共存配置MyBatis-Plus 最爽的地方是单表 CRUD 完全不用写 SQL。比如床位列表的状态筛选一行代码ListBedInfo list bedInfoMapper.selectList( Wrappers.BedInfolambdaQuery() .eq(BedInfo::getStatus, status) .orderByAsc(BedInfo::getFloorId) );用 lambda 而不是硬编码字符串列名最大的好处是编译期就能发现字段名写错的低级问题重构字段名时也不怕遗漏。但项目里并非所有查询都能靠单表搞定。统计报表场景必须手写 SQL。比如按月份统计各护理等级入住人数select idcountByMonthAndLevel resultTypecom.nursing.dto.ElderStatDTO SELECT DATE_FORMAT(c.create_time, %Y-%m) AS month, e.health_level AS healthLevel, COUNT(*) AS total FROM check_in_record c LEFT JOIN elder_info e ON c.elder_id e.id WHERE c.create_time #{startTime} GROUP BY month, e.health_level ORDER BY month /select所以我选择让 Mapper 接口和 XML 文件共存。这里的配置有一个高频坑默认约定下MyBatis-Plus 扫描 XML 的路径是classpath*:mapper/*.xml。如果 XML 和 Mapper 接口不在同一个包下必须在application.yml里明确指定mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl就这个配置我在好几个项目里看到有人漏配结果接口报Invalid bound statement (not found)排查半天才发现是 XML 没被扫描到。3.4 分页、条件检索和导出报表列表页的分页直接用 MyBatis-Plus 的Page对象但要注意分页插件必须先配置拦截器Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }不配这个拦截器page方法只能查出全部数据再内存分页数据量一大页面直接卡死。这是一个典型的“看起来代码没错实际全量查库”的坑。条件检索我这里统一用 DTO 接收参数配合PageElderVO实现多条件分页查询。考虑到老人姓名、身份证、手机号都是模糊搜索字段前端输入什么就拼什么条件MyBatis-Plus 的like方法默认会在首尾加百分号用起来非常顺手。导出报表用的是 EasyExcel 而不是传统 POI。EasyExcel 在内存占用上做了流式处理十万行级别的老人台账导出也不会 OOM。如果项目里用的是原生 POI 且导出的数据量过了万行你会明显感受到 GC 压力暴增。这个取舍值得记一下。4. Vue3 前端实现搭框架、配路由、控权限、联调4.1 Vite Element Plus 工程搭建前端用 Vite 创建 Vue3 项目后我做的第一件事是统一目录规范和请求封装。这里有个跟后端对应的设计前端也按业务模块分目录views/elder、views/room、views/fee而不是按页面类型堆在一起。Element Plus 按需引入避免全量打包导致的体积膨胀。配置unplugin-auto-import和unplugin-vue-components后组件和 API 都能自动按需加载开发时不用手动 import 每个组件代码干净不少。登录页到首页的过渡我是这样处理的登录成功拿到 token 后先调用一次getUserInfo接口拿到用户角色和菜单列表根据角色动态注册路由而不是一次性注册所有路由再把没权限的菜单藏起来。这样做的真正好处是资源层面的隔离——没有权限的页面组件根本不会加载而非只是“看不见”。4.2 axios 封装与统一响应处理axios 封装的核心不是多写几行代码而是把错误处理收敛到一处。项目里统一了后端返回结构{ code, message, data }axios 响应拦截器的逻辑就变得很清晰service.interceptors.response.use( (response) { const res response.data; // 业务码为200表示成功其他情况交给统一错误处理 if (res.code 200) { return res; } if (res.code 401) { // token失效清除本地登录态并跳转登录页 ElMessage.error(登录状态已过期请重新登录); router.push(/login); return Promise.reject(new Error(res.message)); } ElMessage.error(res.message || 系统错误); return Promise.reject(new Error(res.message)); }, (error) { // 处理HTTP层错误如500、404、网络超时 ElMessage.error(error.response?.data?.message || 网络异常); return Promise.reject(error); } );这里特别强调401 必须和 200 区分处理。很多管理系统的 token 过期后前端只是弹个错误用户还停留在页面后续请求全部失败体验非常割裂。统一跳到登录页是必要的。4.3 动态路由与按钮权限按钮权限我用的是自定义指令v-permission。后端返回当前用户拥有的权限标识列表前端把权限数组存入 Pinia。使用方式el-button v-permissionelder:delete typedanger删除/el-button自定义指令内部校验如果没有权限移除该按钮的 DOM 节点并提示。这种方式比用v-if在每个页面里写判断更优雅权限变更只改后端返回的权限列表前端页面代码零改动。4.4 实际页面开发老人档案表格与床位可视化老人档案列表页是整个系统中字段最多的页面。表格列包含姓名、性别、年龄、护理等级、入住房间、床号、家属联系方式、状态等。考虑到老人信息字段较多列表页我采用了“表格 行内下拉详情抽屉”的设计点击某一行弹出抽屉展示完整档案和健康记录避免一进列表页就铺满几十个字段的大表格这种体验在窄屏上尤其重要。床位可视化页是另一个比较有代表性的场景。房间按楼层分组展示每个房间渲染成一个卡片里面放了床位网格。床位状态用颜色区分绿色空闲、橙色已住、灰色停用。点击空闲床位可以弹出“入住登记”对话框同步完成选床和建档案的操作。这个页面是业务方最满意的功能因为它直接解决了线下“哪个床位是空的”这个高频痛点。组件的 props 传递和事件向上抛我选择用 defineProps defineEmits 的显式声明方式而不是用 provide/inject 到处传状态保证组件的可复用性——一个RoomCard组件以后要放到别的页面用拿过来就能跑。5. MySQL8.0 环境部署与性能细节5.1 MySQL8.0 安装与初始化的几个关键动作MySQL8.0 相比 5.7 在安装和初始化流程上有几个变化。如果你是在 Linux 服务器上用 Docker 安装比较省心的做法是直接拉镜像跑容器注意几个参数docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDyourStrongPwd \ -e TZAsia/Shanghai \ -v /opt/mysql8/data:/var/lib/mysql \ -v /opt/mysql8/conf:/etc/mysql/conf.d \ mysql:8.0这里最容易忽略的是-e TZAsia/Shanghai。如果不设置容器时区MySQL 的CURRENT_TIMESTAMP会默认使用 UTC 时间写入的时间比北京时间慢 8 小时。项目联调阶段一旦出现这个 bug时间数据已经污染了一部分处理起来很被动。本地开发环境要装的话注意 8.0 的初始化和 5.7 有区别MySQL8.0 默认使用caching_sha2_password认证插件老版本客户端比如 5.x 的驱动连接时会报认证协议不兼容。想让旧工具也能连接可以改成mysql_native_passwordALTER USER root% IDENTIFIED WITH mysql_native_password BY yourPwd;不过从长远看推荐直接升级使用支持 8.0 的客户端和驱动而不是把认证插件降级。5.2 连接配置、时区、字符集这些坑后端application.yml的数据库连接配置我给出下面这份经过验证的版本spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/nursing_home?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: yourPwd逐个参数解释characterEncodingutf8保证中文字符集正确存储配合数据库utf8mb4使用。serverTimezoneAsia/Shanghai解决 Java 驱动与数据库服务器时区不一致导致的偏移问题。useSSLfalse本地开发环境一般没有配置 SSL 证书不关掉会报 SSL 连接告警。allowPublicKeyRetrievaltrueMySQL8.0 使用 caching_sha2_password 插件时客户端首次连接需要获取 RSA 公钥这个参数不配某些驱动版本会连接失败。数据库层面建议统一用utf8mb4而不是utf8。前者是真正的四字节字符集支持生僻字和 emoji。老人姓名里偶尔会有生僻字用 utf8 存进去直接变成乱码或者插入失败这类问题一旦上线再改字符集代价非常大。5.3 索引设计与慢查询优化系统数据库里数据量最大的表是护理记录表和操作日志表一个失能老人一天可能有好几条护理记录年累积量轻松到数十万行。所以表设计的时候就要把查询频率最高的条件作为索引前缀elder_info表(name, status)联合索引覆盖列表页最常用的姓名搜索和状态筛选。nursing_record表(elder_id, record_time)索引支撑“某老人的护理时间线查询”。operation_log表(operator_id, create_time)索引支撑审计查询。开发阶段就把常见查询的执行计划过一遍用EXPLAIN看 key 是不是生效了。这个习惯比上线后被动看慢查询日志高效很多。老项目里经常看到status和type这种区分度极低的字段单独建索引索引却完全不被启用白白浪费写放大开销这类低级问题在设计阶段就要避免。6. 实测中踩过的坑和收尾经验6.1 时间类型序列化问题后端实体里我用的是LocalDateTime前端拿到的是 ISO 格式字符串如2025-03-15T14:30:00在 Element Plus 的日期组件里直接显示会变成中间的T非常丑而且表格里时间显示也不符合中文习惯。解决方案是统一配置 Jackson 的序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8但要注意date-format对LocalDateTime不一定生效。更靠谱的做法是全局配置类Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }这个坑在前后端联调时几乎必现最好在项目初期就定好全局时间格式规范否则后面排查起来处处碰雷。6.2 MyBatis-Plus 更新策略导致字段被覆盖MyBatis-Plus 的updateById默认只会更新非 null 字段这个特性大部分时候很方便但也暗藏一个陷阱如果某个字段的值就是要更新成 null这个方法直接失效字段值保持原样。比如退住时把room_id置空释放床位elderInfo.setRoomId(null); elderInfoMapper.updateById(elderInfo);这段代码根本不会把room_id更新为 null因为 MP 默认的字段策略就是忽略 null。处理方案有两种一是给该字段加注解TableField(updateStrategy FieldStrategy.IGNORED)二是直接用LambdaUpdateWrapper手动 set nullelderInfoMapper.update(null, Wrappers.ElderInfolambdaUpdate() .set(ElderInfo::getRoomId, null) .eq(ElderInfo::getId, elderId));第二种方式更显式一点代码读起来也清楚。这类问题不实际跑业务很难发现因为大部分更新操作确实不涉及置空唯独退住换房这种场景会触发。6.3 ThreadLocal 内存泄漏前文提到的UserContext用 ThreadLocal 保存登录用户信息如果不在拦截器afterCompletion中清理Tomcat 的工作线程复用时下一次请求可能会读到上一个用户的身份信息。一旦发生轻则数据混乱重则出现越权操作。排查这类问题很折磨人因为不是必现而是跟线程调度有关。正确写法必须兜底清理Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); }同时也建议在preHandle里先UserContext.clear()再设置双保险。6.4 跨域与前端部署配置开发环境下前端跑在 5173 端口后端跑在 8080跨域不可避免。本地联调我通过 Vite 的 proxy 把请求转发到后端规避浏览器跨域限制// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, } } }生产环境则完全不需要在代码层面处理跨域前后端部署在同一域下Nginx 将/api反向代理到后端服务即可。很多初学者上来就配全局 CORS、把allowed-origins设成*这在生产环境是安全隐患。跨域配置只应该服务于特定的开发联调或特殊网关场景。6.5 项目文档到底该沉淀什么这套项目我按四个文档整理需求说明、数据库设计、接口文档、部署手册。写文档的原则不是越多越好而是每个文档都能回答一个特定角色的问题新接手开发的人员看接口文档能知道每个接口出入参部署人员看部署手册能一步不落地把环境搭起来产品或业务方看需求说明能确认功能覆盖后续做版本迭代的人看数据库设计能直接理解字段含义。接口文档我直接在代码里用 Swagger 注解维护生成在线文档避免单独维护一份 Markdown 导致接口变了文档没同步的问题。数据库设计文档则放在docs/目录另外导出一份 SQL 初始化脚本确保任何人拿到源码都能用最短时间把环境拉起来。这一点在源码分享和团队协作中非常加分。最后分享一点个人经验项目收尾之后我最大的体会是管理系统真正难的不是技术实现而是把业务状态和边界梳理清楚之后再动手写代码。这套敬老院系统最大的投入在数据库设计和业务流转上技术栈用的都是非常成熟稳定的方案但它能解决的问题、能覆盖的真实业务场景才是它真正的价值所在。如果你打算拿这套源码学习或二次开发我的建议是先把 SQL 初始化脚本完整跑一遍然后用 Swagger 文档把老人档案和入住退住这两个核心流程的接口挨个调一遍带着业务逻辑去看代码比单纯刷代码要有效得多。你也可以在此基础上扩展一个家属端微信小程序或者 App把健康档案和缴费通知推送给家属这一块现在不少养老机构的需求都集中在这里。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门