☰
SpringBoot+Vue智慧养老监护平台实战:从前后端分离到工单闭环
2026/9/28 12:09:46 网站建设 项目流程

做社区信息化项目这些年,我接触过不少养老相关的系统需求,从早期用PHP拼出来的单体应用,到后来SpringBoot+Vue前后端分离的完整平台,踩过不少坑,也沉淀了不少经验。今天要聊的这套社区智慧养老监护管理平台,就是一个很典型的Java全栈实战项目:后端SpringBoot+MyBatis操作MySQL,前端Vue做交互界面,覆盖老人档案、健康监测、异常告警、护工工单、排班和家属查看等完整闭环。它适合两类人看:一类是想系统学习前后端分离项目如何落地的Java开发者,另一类是有养老信息化需求、想快速搭建参考原型的团队。这篇文章我尽量把项目拆透了讲,从需求分析到表结构,从核心代码到常见坑,一次性说清楚。

先说说这套平台存在的意义。社区养老场景下,护工人手有限,老人健康数据散落在血压计、心率手环和纸质记录本里,一旦出现异常,从发现到处警的链路很长。平台要做的,就是把分散的数据统一收拢,用规则自动识别风险,再把处置任务以工单形式派给对应护工,同时让家属端能实时看到情况。说白了,就是用一条可追溯的线上流程,替代原先靠喊、靠跑、靠人肉记录的管理方式。

1. 项目拆解:社区养老平台到底要解决什么问题

1.1 从线下纸质流程看系统的价值锚点

我见过不少社区日间照料中心,规模不大,几十张床位,三四个护工。每天早晨的工作是给老人量血压、测心率,数据记在一个本子上,谁高谁低全凭脑子记。真有老人状态不好,护工先喊同事,再找值班负责人,最后打电话通知家属,中间任何一环人不在,响应就断了。这套流程的问题不在于人不够敬业,而在于数据不集中、流程不标准、责任不可追溯。

所以做智慧养老监护平台,第一个要解决的就是数据在线化。老人入住后建立完整电子档案,包括基础信息、既往病史、过敏史、健康评估等级;每天的体征数据通过设备采集或护工录入,自动落到健康记录表;系统按预设规则判断数据是否异常,异常时生成告警,再转成工单指派给当班护工。整个链路从数据产生到处置完成都有时间戳和操作人记录,出了问题能回溯,家属的知情权也有了保障。

第二个要解决的是资源调度问题。社区养老最怕的是老人突发状况找不到人。通过排班模块,管理员可以提前安排好每个时段的护工力量;工单系统则确保每一个告警都有明确责任人。如果护工超时未处理,系统自动升级提醒,避免"以为有人管、实际没人管"的情况。

1.2 核心角色与业务流程梳理

这个平台涉及四类角色,权限边界很清楚:

  • 超级管理员:负责系统配置、账号分配、全局数据查看;
  • 社区运营人员:管老人档案、护工排班、工单调度和告警确认;
  • 护工:通过移动端或PC端接收工单、录入健康数据、反馈处置结果;
  • 家属:查看老人健康趋势、接收告警通知、了解护理记录。

业务流程我梳理一下,这条主链贯穿了系统所有核心表:

老人入住登记 → 健康评估与等级划分 → 分配床位和设备绑定 → 日常健康数据采集 → 规则引擎判断异常 → 生成告警记录 → 关联排班匹配护工 → 生成工单 → 护工处理并反馈 → 运营人员审核关闭 → 家属端同步查看 → 周期性健康报告生成

这套流程下来,技术上其实没有什么高深算法,难的是把每个环节的状态管理清楚。比如告警状态有未确认、处理中、已关闭,工单状态有待派单、处理中、已完成、超时升级,每一步流转都要有对应的表字段和接口逻辑。

1.3 功能模块地图

模块主要功能点说明
老人管理档案CRUD、状态流转、健康评估、家属绑定状态含待入住/已入住/暂停/退住
健康监测体征数据录入、设备数据导入、趋势图、异常标记心率/血压/血氧/体温
告警管理规则配置、告警生成、确认处理、升级机制阈值可配置,分级管理
工单管理工单生成、派单、处理反馈、超时提醒与排班联动,自动匹配护工
排班管理日历排班、周排班、班次管理护工和时段的映射关系
系统管理用户、角色、菜单、日志RBAC权限模型
统计报表老人分布、健康趋势、工单完成率ECharts图表展示

2. 技术选型解析:为什么是SpringBoot+Vue+MyBatis+MySQL这套组合

2.1 版本选型:SpringBoot 2.7还是3.x,Vue 2还是Vue 3

这套项目能流行,很大程度上因为技术栈足够主流,学习资料多,遇到问题好查。但版本选择上,我建议别盲目追新。

后端如果做毕业设计或者给中小企业做落地项目,我个人倾向SpringBoot 2.7.x + JDK 8,原因很简单:生态兼容性最稳。SpringBoot 3.x 强制要求JDK 17,虽然性能有提升,但很多老项目的依赖要跟着升版,比如Springfox迁移到springdoc、MyBatis-Plus要用新版,折腾成本不小。当然,如果这是全新项目而且团队对JDK 17已经很熟,那直接用3.x也没问题,代码写法和2.7差别不大。

前端这边,Vue 3 + Element Plus已经是新项目的默认选择。Vue 2虽然还有大量存量项目,但官方维护已进入末期。Vue 3的组合式API写业务逻辑比选项式API更清晰,一个setup函数里就能把数据、方法、生命周期组织在一起,不用再data和methods两头跳。配套的Vite构建速度快,开发体验比webpack时代舒服很多。

MySQL版本建议8.0以上,默认字符集utf8mb4,支持JSON类型和窗口函数。8.0的窗口函数在做健康趋势报表、工单统计时非常有用,比如计算每位老人的体征均值、排名等,一条SQL就能搞定,不用在Java里循环处理。

2.2 MyBatis与MyBatis-Plus的组合拳

为什么不用JPA?社区养老这类管理系统,复杂统计报表多,类似"按周统计每位老人的平均心率并对比上周"的查询,用MyBatis的XML写SQL更有掌控感。JPA虽然能自动建表,但一旦涉及多表关联、动态条件、复杂聚合,要么写JPQL,要么用原生SQL,反而不如MyBatis直观。

项目实际开发中,我建议直接用MyBatis-Plus,它相当于是MyBatis的增强工具包,内置了单表CRUD、分页插件、逻辑删除、乐观锁和代码生成器。单表操作不用写Mapper XML,直接继承BaseMapper调用selectById、insert等现成方法;复杂SQL还是保留XML手写,两边互补。

一个典型的MyBatis-Plus配置类:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); // 设置最大单页限制,防止全表扫 pagination.setMaxLimit(500L); // 溢出总页数后是否回到第一页 pagination.setOverflow(false); interceptor.addInnerInterceptor(pagination); return interceptor; } @Bean public MetaObjectHandler metaObjectHandler() { return new MetaObjectHandler() { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }; } }

这里有个细节:分页拦截器加了最大单页限制,防止前端把pageSize改成999999导致数据库压力骤增。MetaObjectHandler自动填充创建时间和更新时间,省去每张表手动set当前时间的重复劳动。

2.3 数据库设计:健康记录表的核心思路

表结构是这类系统的灵魂。我挑数据量最大、设计最关键的health_record表说。

CREATE TABLE `health_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `elder_id` bigint NOT NULL COMMENT '老人ID', `heart_rate` int DEFAULT NULL COMMENT '心率 次/分', `sbp` int DEFAULT NULL COMMENT '收缩压 mmHg', `dbp` int DEFAULT NULL COMMENT '舒张压 mmHg', `blood_oxygen` decimal(4,1) DEFAULT NULL COMMENT '血氧饱和度 %', `temperature` decimal(4,1) DEFAULT NULL COMMENT '体温 ℃', `source_type` tinyint DEFAULT '1' COMMENT '数据来源 1设备 2手工录入', `device_code` varchar(32) DEFAULT NULL COMMENT '设备编号', `measured_at` datetime NOT NULL COMMENT '测量时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_elder_measured` (`elder_id`, `measured_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康体征记录';

这里最关键的是联合索引idx_elder_measured,因为查询几乎都是按老人ID和时间范围来做的,比如"查某位老人最近7天的血压趋势"或者"查某个时段所有异常数据"。如果不建这个索引,数据量上来以后,这种查询会频繁触发全表扫描。

告警规则表我另建一张rule_config,字段大致是measure_type(指标类型,比如心率)、min_value、max_value、alarm_level(黄色/橙色/红色)、enable。这样要调整阈值不用改代码,运营人员直接在管理端改了就能生效。实际项目里血氧低于92、收缩压高于140、心率低于50或高于100这些都是常见的默认阈值。

3. 核心业务模块与关键代码实现

3.1 老人档案与状态流转

老人不只是简单的新增和修改,状态流转是整个模块最需要谨慎处理的地方。老人状态我设计了四种:待入住、已入住、暂停服务、已退住。比如老人请假一星期去子女家住,不能直接退住,应该进入暂停服务状态,对应的排班和设备绑定暂时解绑,回来再恢复。

状态变更不能只改一个字段就完事,要记录变更日志。我专门建了status_change_log表,字段包含elder_id、from_status、to_status、change_reason、operator_id、create_time。这样家属或者管理人员问起"这位老人为什么从上个月开始状态变成暂停了",能直接查日志给出答案,而不是靠人工回忆。

状态变更的核心Service方法大概是这个思路:

@Transactional(rollbackFor = Exception.class) public void changeElderStatus(Long elderId, Integer targetStatus, String reason, Long operatorId) { Elder elder = elderMapper.selectById(elderId); if (elder == null) { throw new BusinessException("老人档案不存在"); } Integer oldStatus = elder.getStatus(); // 校验状态流转是否合法,比如退住后不能直接恢复为已入住,必须走重新入住流程 validateStatusTransition(oldStatus, targetStatus); elder.setStatus(targetStatus); elderMapper.updateById(elder); StatusChangeLog log = new StatusChangeLog(); log.setElderId(elderId); log.setFromStatus(oldStatus); log.setToStatus(targetStatus); log.setChangeReason(reason); log.setOperatorId(operatorId); statusChangeLogMapper.insert(log); }

状态机校验这里容易踩坑:如果不在Service层做合法性校验,就会出现数据错乱,比如退住老人还在接工单。实际开发中可以用枚举定义状态的允许流转矩阵,写起来清晰很多。

3.2 健康数据采集与告警规则引擎

健康数据是平台的心脏,采集频率通常是每5分钟一批。如果一个社区有100位老人、几十台设备,一天下来就是上万条记录。直接逐条insert很浪费连接资源,我用的是MyBatis-Plus的批量插入能力,配合分批提交,每次1000条左右。

设备端推送的数据一般走消息队列或者HTTP接口。为了保持项目简单,我用了WebSocket主动推送,让前端大屏实时刷新健康数据;如果团队想控制复杂度,也可以用前端定时轮询接口,这个项目采用长轮询加定时刷新方案,实现起来简单,量级也能支撑。

告警规则引擎是核心。规则配置存在数据库,判断逻辑放在Service里。每次采集到新数据后,不再用一堆if-else在业务代码里写死阈值,而是遍历启用的规则配置,逐条匹配:

public void evaluateRules(HealthRecord record) { List<AlarmRule> enabledRules = alarmRuleMapper.selectList( new LambdaQueryWrapper<AlarmRule>().eq(AlarmRule::getEnable, true) ); for (AlarmRule rule : enabledRules) { Double value = extractMeasureValue(record, rule.getMeasureType()); if (value == null) { continue; } boolean trigger = false; if (rule.getMinValue() != null && value < rule.getMinValue()) { trigger = true; } if (rule.getMaxValue() != null && value > rule.getMaxValue()) { trigger = true; } if (trigger) { createAlarm(record.getElderId(), rule); } } }

告警生成后,系统会做一次去重:同一老人在同一时间段内同一指标连续异常,只生成一条告警,防止告警风暴。这个去重逻辑我放在告警生成前,查询是否有未关闭的同类型告警,有则合并。

3.3 工单闭环与排班联动

告警只是发现问题,处置才是核心。我按工单状态流转来管理处置链路:待派单 → 处理中 → 已完成 → 已关闭。如果再细分,还可以有超时升级状态。

自动派单的逻辑是:根据告警老人的床位所在区域,查询当天该区域的排班记录,找到当班护工,生成工单并推送。如果排班没有匹配到护工,则工单进入待派单池,由运营人员手动指派。

排班和工单联动这块,有个非常实用的点:默认告警级别是黄色,需要在30分钟内确认;橙色15分钟;红色5分钟。系统定时扫工单,如果超时未确认,自动通知运营人员,并给当班护士长生成一条升级任务。这个功能有效解决了线下"人不在岗没人管"的痛点。

我用SpringBoot自带的@Scheduled实现定时扫描,代码示例:

@Component public class WorkOrderTimeoutTask { @Scheduled(cron = "0 */1 * * * ?") public void checkTimeoutOrders() { List<WorkOrder> pendingOrders = workOrderMapper.selectList( new LambdaQueryWrapper<WorkOrder>() .in(WorkOrder::getStatus, Arrays.asList(0, 1)) ); LocalDateTime now = LocalDateTime.now(); for (WorkOrder order : pendingOrders) { Integer level = order.getAlarmLevel(); Integer limitMinutes = getLimitMinutesByLevel(level); if (now.isAfter(order.getCreateTime().plusMinutes(limitMinutes))) { // 升级处理 upgradeWorkOrder(order.getId()); } } } }

注意这里的cron表达式是每分钟执行一次,生产环境建议用分布式锁保证多实例部署时只有一个节点执行,避免重复扫描。简单方案可以引入Redis的setnx锁,或者用ShedLock这类专门给定时任务加锁的库。

4. 从零跑通项目的实战记录

4.1 环境准备与项目初始化

先说环境:JDK 8(或JDK 17)、Maven 3.6+、Node.js 16+、MySQL 8.0。开发工具我习惯用IntelliJ IDEA,前端用VS Code。数据库连接工具推荐Navicat或DBeaver。

后端工程创建,直接在IDEA里用Spring Initializr选择SpringBoot 2.7.x,依赖选Lombok、Spring Web、MyBatis Framework、MySQL Driver。注意MyBatis Framework这里先别选MyBatis-Plus,因为Spring Initializr默认生成的是官方starter,后面需要手动引入MyBatis-Plus依赖。

pom.xml里加上关键依赖:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency>

前端工程用Vite创建:

npm create vite@latest elder-web -- --template vue cd elder-web npm install npm install element-plus axios vue-router@4 pinia echarts

4.2 application.yml关键配置

spring: datasource: url: jdbc:mysql://localhost:3306/elder_care?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 30000 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath:mapper/**/*.xml configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

这里三个细节值得留意。第一是数据库连接URL一定要加serverTimezone=Asia/Shanghai,不然8小时时差问题会让前端展示的时间全错。第二是jackson的日期格式和时间时区要显式配置,否则LocalDateTime默认会被序列化成数组或ISO格式。第三是逻辑删除字段deleted,所有业务表统一加上这个字段,删除操作自动变成update,数据保留可追溯。

4.3 后端核心接口实现

后端我按controller、service、mapper三层组织。业务接口的Controller只做参数接收和结果封装,不写业务逻辑。统一返回结构我用了一个Result类,code、message、data三个字段,前端拦截器统一判断。

健康数据查询接口是高频接口,做了一个典型的分页查询:

@GetMapping("/health-records") public Result<IPage<HealthRecordVO>> pageHealthRecords( @RequestParam Long elderId, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "20") Integer pageSize, @RequestParam(required = false) String startTime, @RequestParam(required = false) String endTime) { LambdaQueryWrapper<HealthRecord> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(HealthRecord::getElderId, elderId) .between(StringUtils.isNotBlank(startTime) && StringUtils.isNotBlank(endTime), HealthRecord::getMeasuredAt, startTime, endTime) .orderByDesc(HealthRecord::getMeasuredAt); IPage<HealthRecord> page = healthRecordMapper.selectPage( new Page<>(pageNum, pageSize), wrapper); return Result.success(page); }

注意这里的between条件用了一个布尔表达式作为第一个参数,这是MyBatis-Plus支持的条件组装方式。当startTime和endTime为空时,这个条件自动不拼接,非常方便,不用再手写一堆if判断。

4.4 前端Vue页面实现

前端这边,Axios封装是重点。拦截器统一处理token注入和401跳转:

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } )

路由守卫控制页面权限:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) { next('/login') } else { next() } })

页面层面,健康数据大屏是最出效果的模块。用ECharts做趋势折线图,选某个老人后,查询最近7天的血压数据,横坐标是测量时间,纵坐标是血压值,超出阈值范围的数据点用红色标记。Vue 3里用echarts的写法是先在setup里引入echarts,再通过ref绑定的DOM实例做初始化。图上还可以叠加一个深色警戒区间,比如收缩压140到160之间的区域用浅黄色背景,让家属一眼看出哪段数据偏高。

5. 项目落地时的常见坑与排查手册

5.1 分页插件不生效?八成是这个原因

MyBatis-Plus的分页插件要生效,拦截器必须作为内置拦截器加入。常见问题有两个:一是直接把PaginationInnerInterceptor单独new出来放进List,没有通过MybatisPlusInterceptor包裹,导致分页不走;二是多个拦截器的顺序不对,如果同时用了动态表名或者数据权限拦截器,注意顺序。

排查方法很简单:打开SQL日志,看执行的语句是否带了LIMIT关键字。如果带了limit但没有正确解析,说明Count语句生成了但又被覆盖了;如果完全没有limit,那就是插件没生效。检查的时候顺便确认一下mybatis-plus的版本,3.5.x和3.4.x在配置写法上略有差异,网上很多老教程写的还是3.4之前的配置方式。

5.2 健康数据高频写入的数据库压力

健康数据批量插入是这类系统最容易出性能问题的点。我实测5分钟一批1000条记录,单表单条插入和批量插入的性能差距在百倍级别。解决方案有三个方向:

一是批量插入合并为一条INSERT语句,MyBatis-Plus的saveBatch默认就是先拆成多条再逐条执行,需要配置rewriteBatchedStatements=true才能真正批量。我在MySQL连接串上加了这个参数之后,耗时从十几秒降到一秒以内。

二是考虑引入Redis做缓冲。设备数据先推到Redis的List或者Stream里,定时任务批量消费写入MySQL。这样即使设备瞬时上报量很大,数据库也不会被直接打满。对于中小项目,每批次1000条已经够用,不一定非上Redis。

三是对历史数据做归档。health_record表越来越大后,按月分表或者定期把超过6个月的冷数据迁移到history表。分表方案对查询逻辑会有一点侵入,需要根据路由字段处理,初期建议先做归档。

5.3 跨域、时区、序列化三件套

前后端分离项目,跨域问题几乎必踩。如果后端接口在8080,前端Vite在5173,浏览器会拦截。后端的处理方式我一般配置一个CorsFilter,允许指定前端的来源访问,而不用全局*号。SpringBoot 2.7里可以用WebMvcConfigurer的addCorsMappings统一配置。同时在Spring Security的配置里也要同步放行OPTIONS预检请求,否则前端请求直接被拦截。

时区问题前面提过,连接URL里的serverTimezone=Asia/Shanghai务必加上。日期返回格式,如果实体里用的是LocalDateTime,Jackson需要加jackson-datatype-jsr310依赖,然后配置date-format。否则前端拿到的可能是一大串数组,或者格式是"2025-01-15T10:30:00"而不带毫秒精度,页面显示很不友好。

序列化的另一个大坑是MyBatis-Plus返回的分页对象Page用JSON序列化后,会把records和total等字段正常返回,但前端如果用了这套项目自带的类型定义,要注意Page里的records字段在小写开头的JSON里是否被正确解析。我建议统一用VO对象做转换,不要在Controller里直接把实体类返回前端,这样字段可控、也避免把不该暴露的内网字段泄漏出去。

5.4 部署阶段的注意事项

部署上,后端打jar包用java -jar运行,前端构建后放nginx。有几个细节值得说:

第一个是nginx需要配置反向代理转发/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; }

前端请求的baseURL是/api,后端Controller的路径统一以/api开头,这个约定在联调时非常省心。注意proxy_pass末尾的斜杠,它会替换掉匹配到的/api前缀,如果配置不当会出现404。

第二个是数据库连接池。MyBatis-Plus默认使用HikariCP,我建议显示配置maximum-pool-size,一般20个连接足够支撑几百个老人规模的社区。如果高峰时段连接不够,看日志里有没有"Connection is not available, request timed out"的报错。

第三个是SpringBoot的打包。用spring-boot-maven-plugin打包,但注意排除测试类,不然测试代码里如果连了开发库,打包过程中会有中断风险。建议deploy前跑一遍完整测试,没问题再跳过测试打包。

最后分享几点我的实操体会

这套项目做完,我最深的感受是一个管理系统能不能被运营人员真正用起来,关键不在于界面多炫、技术多新,而在于流程是否符合实际工作习惯。健康监测和告警做得再漂亮,如果工单派发不合理,护工看得到却处理不了,最终这个功能会被弃用。所以后来我调整设计时,把排班和工单的联动放在第一优先级,宁可放弃一些花哨的图表,也要保证"告警发出来有人接、工单生成后看得见、处理完有记录"这三点稳定可靠。

另外想提醒一点:养老行业的数据合规和隐私保护要提前考虑,老人健康数据属于敏感个人信息,系统里照片、身份证号、健康记录等字段都需要做权限控制和脱敏展示。技术层面做到字段权限隔离,管理层面做好账号审计日志,这是这类项目真正能不能落地的一个隐藏门槛。

如果你准备拿这套项目做二次开发,我的建议是先跑通老人档案、健康数据、告警和工单这条主线,其他模块比如排班、报表、家属端可以后续迭代。主线跑通,系统的骨架就立住了,再往上面加房间管理、设备管理、收费管理都会很顺。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询