intf对接BPMN工作流引擎:权限校验与操作日志公共封装实践
2026/9/16 6:52:08 网站建设 项目流程

这个题目一看就是企业内部系统开发里绕不开的活儿。项目审核要过流程,流程引擎选了 bpmn,但权限怎么卡、操作怎么留痕,如果每个业务系统各写一套,后期维护绝对想骂人。所以标题里的“intf 对接 bpmn 与公共封装”其实是两件事:一件是接口层怎么跟 bpmn 流程引擎通信,另一件是把权限校验和操作日志抽成公共能力,让所有走审核的业务模块共用。这篇文章就围绕这两条主线,把设计思路、核心实现和踩坑记录完整梳理一遍。

1. 内容整体设计与思路拆解

1.1 核心需求解析:审核、权限、日志三者为何要联动

先理清楚这个项目的本质。项目审核不是一个单纯的状态流转,它天然包含三个维度的诉求:流程要按既定规则走,比如一级审核、二级审核、驳回、会签;每个节点谁能看、谁能批,这是权限;每一次提交、通过、驳回,甚至中间态的数据变更,都要能追溯,这是日志。三者缺一个,审核体系就不完整。

但很多团队在做的时候会把这三件事拆开做。流程引擎单独部署一套,权限在业务代码里用拦截器硬编码,日志靠每个业务表加几个审计字段。短期看没问题,长期看问题很大。比如流程节点调整了,权限拦截逻辑要跟着改;业务系统从 5 个涨到 15 个,每个系统的日志格式都不一样,审计排查的时候要把十几个系统的表拉出来对。所以这个项目从一开始就定了一个调子:intf(接口层)统一对接 bpmn,权限和日志做成公共封装,任何业务模块接入审核能力时,只需要关心自己的业务字段,其余全部走公共能力。

1.2 方案选型:为什么用 bpmn 而不是自研状态机

技术选型的时候有过争论。团队里有人提出,项目审核的场景无非就是提交、通过、驳回、撤回,用一张状态表加一个状态机就能搞定,没必要引入 bpmn 这么重的引擎。这个说法有道理,但只适用于单一、固定的审核路径。

实际情况是,不同业务线的审核规则差异很大。有的需要二级审核,有的需要三级,有的特定金额以上要加签,有的需要指定角色会签。如果用状态机硬编码,每来一个新规则就要改代码、发版、重新测试。而 bpmn 的最大价值在于把流程定义从代码里剥离出来,变成一个可视化的、可动态调整的流程文件。审核节点怎么编排、网关怎么路由、条件怎么判断,全部在 .bpmn 文件里维护,代码层面只关心“当前节点是什么、动作是什么、下一步去哪”。

另一个关键考量是审批痕迹的完整性。bpmn 引擎自带流程实例、任务实例、历史记录,这些数据天然就是操作日志的原材料。自研状态机如果要做到同等程度的追溯能力,开发成本会非常高。所以最终选择了 bpmn,并且把所有对流程引擎的调用统一收敛在 intf 层,避免业务系统直接操作引擎内部表。

1.3 公共封装的边界:哪些能力下沉,哪些留给业务

公共封装最容易犯的错误是封装过度。把什么都做成公共的,业务方反而觉得难用。这个项目在立项时就明确了边界:与业务无关的能力全部下沉,与业务相关的能力通过扩展点暴露给业务方。

下沉到公共封装的能力包括:用户身份解析与权限校验、操作日志的记录与查询、流程任务的统一办理入口、审核结果的统一回调。这些能力的特点是逻辑通用、与具体业务无关。而业务方需要自己实现的包括:审核表单的渲染、业务数据的合法性校验、流程变量中的业务规则映射。这样划分之后,公共封装的接口会非常稳定,业务方接进来时只需要实现少量接口,学习成本低,接入速度快。

2. 核心细节解析与实操要点

2.1 intf 层的数据模型设计

intf 是整个对接的中枢,数据模型设计直接决定了对接的顺畅程度。参考常见实践,我建议在 intf 层定义一套独立的 DTO,不要直接复用 bpmn 引擎的内部对象,也不要直接暴露给业务方。

核心 DTO 包括以下几类。流程发起请求包含业务单据编号、流程定义 Key、业务类型、发起人、流程变量;任务办理请求包含任务实例 ID、办理动作(同意/驳回/转办/加签)、办理意见、流程变量;查询请求包含业务单据编号或任务 ID,用于反查当前流程状态。这些 DTO 统一放在 intf 模块里,业务方依赖这套 DTO 发起和办理审核,intf 内部再把 DTO 转换成 bpmn 引擎的调用参数。

这里有一个非常容易被忽略的细节:流程变量的数据结构。bpmn 的条件表达式依赖流程变量判断网关走向,因此流程变量不能是模糊的“通过/不通过”这种简单布尔值,而应该是一个结构化的审核结果对象。比如包含审核结论、审核金额、是否加签、加签对象等字段。这样在 bpmn 网关配置条件表达式时才能灵活路由,而不是每次加条件都要改代码。

2.2 bpmn 流程设计要点:网关、条件表达式与监听器

流程设计是 bpmn 对接中最容易被低估的部分。很多团队把 bpmn 文件画完就完事了,部署上去才发现网关条件写错、监听器没生效、会签场景处理不了。

网关的使用要特别注意。排他网关和并行网关的语义完全不同。项目审核里最常见的场景是“金额大于 10 万走总监审批,否则经理审批即可”,这种用排他网关。但“财务审核和市场审核同时进行,都通过才能到总经理”这种用并行网关。实际项目中会签和或签的场景也很常见:会签要求多个审批人全部同意,或签只要一人同意即可。bpmn 里实现会签可以用多实例任务,通过 Collection 和 Completion Condition 控制。

条件表达式的写法也要规范。常用的表达式框架是 JUEL 或 SpringEL,常见格式如${amount > 100000}。这里踩过一个坑:流程变量必须是基本类型包装类或者 BigDecimal 类型,如果传的是字符串 “100000”,表达式判断就会出错。建议在 intf 层做参数转换时,就明确数值型流程变量的类型,不要依赖引擎自动转换。

监听器的使用同样关键。这个项目里用了两类监听器:执行监听器和任务监听器。执行监听器配合流程实例的生命周期事件使用,比如流程结束、流程取消;任务监听器配合任务创建、任务完成事件使用。操作日志的落库,建议监听任务完成事件触发,这样可以拿到任务 ID、办理人、办理意见、办理时间,一次性写入日志表。

2.3 公共封装的权限模型设计与实现

权限体系是项目审核中最敏感的部分。审核节点错了人,轻则流程乱走,重则产生越权审批的合规风险。这里采用的方案是基于 RBAC 的扩展模型,组织用户关系 + 角色 + 数据权限范围三层结构。

组织用户关系解决“这个人是谁”的问题。每个用户归属于至少一个部门或小组,通过组织树向上聚合。审核节点配置的审批人来源,优先从角色取,其次从组织取。角色的粒度要够细,比如“部门经理”和“总监”要分开,“财务审核员”和“财务经理”要分开,否则很容易出现角色权限过大或不足的问题。

权限校验的逻辑封装在公共模块里,对外提供两个核心方法:判断当前用户是否拥有某个权限点,如审核、撤回、转办;获取当前用户可操作的审核任务列表,这涉及数据权限。数据权限的实现参考了行级权限的通用做法:通过查询条件动态拼接,用户只能看到自己发起或者自己待办的审核单,管理员可以看到全部。这套逻辑在 intf 层统一封装,业务方无需关心具体的权限过滤条件。

权限与流程的联动是实现时的重难点。一个常见误区是:流程发起时就把后续所有节点的审批人固定写入流程变量。这种做法在组织架构稳定时问题不大,但一旦审批人离职或调岗,已经发起的流程就会卡死。正确做法是:审批人通过表达式动态解析,节点配置写的是角色或组织,运行时通过公共权限接口实时获取当前该角色下的有效用户。这样即使组织调整,未完成的流程也能按最新权限配置继续走。

2.4 操作日志的公共封装:记录什么、怎么记录、如何查询

操作日志的封装要回答三个问题:记录什么内容、通过什么机制记录、记录后的日志如何查询和呈现。

记录内容方面,参考审计日志的通用字段规范,至少包含:操作人、操作时间、操作类型、业务单据编号、流程实例 ID、任务实例 ID、操作详情(JSON 格式)、IP 地址。操作详情里要记录操作前后的关键数据变化。该项目的做法是:审核提交时,业务方传入一个业务数据快照;审核完成时,再上传一个后置快照。公共模块自动比对两个快照的差异,写入日志的变更字段。这样审计人员查看日志时,能直观看到审核前后哪些数据被修改了,不需要去翻业务系统的原始表。

记录机制方面,采用 Spring AOP 注解的方式。公共模块定义一个 @AuditLog 注解,标注在 intf 层的方法上。方法执行前解析注解中的业务类型和日志场景,方法执行后通过环绕通知捕获返回值,组装日志内容,异步写入日志表。选择异步写入是为了避免日志操作影响主流程的性能,但需要注意异步线程池的隔离,避免日志队列堆积影响业务。

日志查询方面,公共模块提供一套通用的查询 API,支持按操作人、业务单据编号、操作类型、时间范围组合查询。查询结果分页返回,同时聚合展示单个审核单的完整流转过程。为了方便审计追溯,建议对日志表按时间按月做分区,并定期归档到冷存储。

3. 实操过程与核心环节实现

3.1 环境准备与依赖引入

参考常见实践,这个项目的基础技术栈以 Java 为主,核心依赖是 Camunda 或 Flowable 这类开源 bpmn 引擎。我实际用下来更倾向 Flowable,因为它的 API 设计相对简洁,Spring Boot 集成资料多,遇到问题容易查。如果你们团队对 Activiti 更熟,用 Activiti 也一样,本文的思路完全适用。

依赖引入时需要注意版本对齐。如果用的是 Spring Boot 2.7.x,Flowable 建议用 6.x 系列。引入依赖时,引擎自带的 MyBatis 版本可能与你们业务系统的 MyBatis 冲突,建议排除掉引擎自带的依赖,统一使用业务系统里的版本。这一步很容易踩坑,我第一次集成时因为 MyBatis 版本冲突,启动直接报错,花了半天排查。

公共封装部分的依赖建议单独抽成一个 Maven 模块,比如 project-common-audit,这样业务系统按需引入,不会强制所有项目都依赖流程引擎。权限、日志相关的工具类放在这个模块里,intf 层依赖该模块,业务系统依赖 intf 层暴露的接口。

另外要提前准备好数据库脚本。流程引擎需要一套自己的表结构,Flowable 默认有几十张表,启动时如果配置为自动建表会比较省事,但生产环境建议手动执行官方 SQL 脚本建表,并把初始化动作关掉。公共封装的日志表、权限表需要单独创建,不要混在业务库里,也尽量与流程引擎表分离,方便职责划分和数据迁移。

3.2 intf 层的核心接口定义与实现

intf 层对外暴露的接口要尽量精简。以常见的项目审核场景为例,需要提供的核心接口如下:

public interface ProjectAuditIntf { // 发起审核 AuditStartResult startProcess(ProcessStartRequest request); // 办理任务(同意/驳回/转办/加签) TaskActionResult handleTask(TaskActionRequest request); // 查询待办任务 PageResult<TaskView> queryTodoList(TodoQueryRequest request); // 查询已办记录 PageResult<TaskView> queryDoneList(DoneQueryRequest request); // 查询流程状态及审批历史 ProcessTraceResult queryProcessTrace(String businessNo); }

每个方法的实现都遵守同样的套路:参数校验、权限校验、调用引擎、组装返回值、记录日志。

参数校验放在最外层。业务方传入的请求对象,先做基础校验和业务校验。基础校验如业务单据号不能为空、操作人不能为空;业务校验由业务方的校验器实现,比如提交审核时强制要求金额必须大于 0。校验失败直接抛出带错误码的异常,公共封装统一捕获后返回给前端。

权限校验的处理在接口层做一层统一拦截。根据操作类型判断当前用户是否有权限:发起流程,要求登录用户必须有“项目发起”权限点;办理任务,要求当前用户必须是该任务的候选人或被指派人;查询流程轨迹,要求当前用户必须是流程发起人或管理员。权限校验逻辑不从业务方传入,而是从登录上下文获取当前用户信息,避免伪造。

引擎调用环节将 intf 的 DTO 转换为 Flowable 引擎参数。发起流程时,合法的流程定义 Key 可以从业务类型映射表读取。办理任务时,对应的引擎 API 要区分动作类型。驳回动作要注意:驳回分驳回上一步和驳回到发起人两种,通过流程变量指定驳回目标节点。会签场景则走多实例任务,调用一端需在流程变量里传入人员清空和完成条件参数。

3.3 权限校验与数据过滤的完整实现

权限校验逻辑不能散落在各个 intf 实现类里,必须用一个公共 AOP 切面统一处理。定义一个 @RequiresPermission 注解,标注在 intf 方法上,注解参数说明需要校验的权限点。AOP 切面在方法执行前解析注解,从当前登录上下文获取用户,调用权限服务判断是否拥有对应权限点。

范围权限的过滤更考验设计。拿待办查询来举例:普通员工只能看到自己的待办;部门经理可以看到本部门所有员工的待办;总监可以看到整个业务线的待办。这个逻辑如果用 SQL 来写,会出现大量 if-else 分支。更好的做法是把范围权限抽象成一个数据权限解析器,输入当前用户,输出该用户可见的组织范围列表,查询时统一用in 组织范围过滤。

这个项目里我的实现是,在任务查询表里冗余了“发起人部门编码”和“当前处理人部门编码”两个字段,查询待办时按当前用户的组织范围做前缀匹配查询。如果当前用户是部门经理,可见范围是“本部门及子部门”,SQL 条件就是任务表.当前处理人部门编码 like '父部门编码%'。这种实现适合组织层级不太深的场景,如果组织深度超过五层,建议用闭包表或者路径枚举的方案。

3.4 操作日志记录的核心实现

日志切面是这个封装里最核心的代码之一。它的核心思路是:拦截 intf 层的方法,在方法执行成功后异步记录日志。

先定义日志注解:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuditLog { // 业务类型,如 PROJECT_AUDIT String bizType(); // 日志场景,如 SUBMIT、APPROVE、REJECT String scene(); }

然后在 intf 实现类的方法上标注注解。切面类里做几件事:

@Aspect @Component public class AuditLogAspect { @Around("@annotation(auditLog)") public Object around(ProceedingJoinPoint joinPoint, AuditLog auditLog) throws Throwable { // 组装基础信息:用户、IP、时间 // 生成日志编号 Object result = joinPoint.proceed(); // 方法执行后异步落库 // 组装操作详情,包括请求参数和响应结果 return result; } }

实际操作中发现一个关键问题:如果只在原方法上做日志,无法记录流程节点完成时的操作详情。比如审批人同意后,引擎自动把流程流转到下一节点,这个过程不一定有 intf 方法调用。所以除了 AOP 切面,还需要结合引擎的监听器埋点。

监听器的实现方式是:在 bpmn 流程文件中,对需要记录日志的用户任务节点,添加 task listener,指定一个公共的监听器类。监听器在任务创建时记录“待办产生”,在任务完成时记录“审批完成”。监听器内部通过解析流程变量,可以拿到业务单据号、审批人、审批意见、审批结果。这样操作日志的覆盖面就更完整了,既有请求维度的日志,也有流程节点维度的日志。

3.5 公共封装模块的工程结构参考

项目落地时,工程模块划分推荐如下结构。这个结构参考了我在实际项目中的经验,既能保证模块边界清晰,又不会过度拆分。

project-intf/ ├── src/main/java/com/example/project/intf/ │ ├── dto/ // 请求、响应参数对象,与业务系统交互 │ ├── service/ // intf 层接口定义 │ ├── impl/ // intf 层实现,集成 bpmn 引擎的入口 │ ├── convert/ // DTO 与引擎参数、实体之间的转换器 │ └── exception/ // 业务异常定义 project-common/ ├── src/main/java/com/example/project/common/ │ ├── audit/ // 操作日志注解与切面 │ ├── auth/ // 权限注解、数据权限解析器 │ ├── context/ // 登录用户上下文 │ └── util/ // 通用工具类 project-business/ └── src/main/java/com/example/project/business/ ├── controller/ // 对外 HTTP 接口 ├── service/ // 业务逻辑 └── repository/ // 数据访问层

实际开发时要注意,intf 层不要直接依赖业务模块,只能依赖公共模块和流程引擎。业务模块通过 Spring 的依赖注入使用 intf 层发布的服务,这样分层足够清晰,后端团队并行开发时互不干扰。后续如果要把 intf 层的能力包装成 HTTP API 或 RPC 服务暴露给其他子系统,只需要在 intf 层上再包一层适配器即可。

4. 常见问题与排查技巧实录

4.1 bpmn 流程部署与版本管理问题

流程引擎部署 bpmn 文件的时机很有讲究。开发阶段可以每次启动自动部署,方便改完流程立即验证。生产环境必须做版本管理,因为一旦发布了流程,线上还有未办结的实例,旧版本流程文件不能随便删除。

具体建议是:流程文件存放在单独的资源目录,部署时给每个流程定义配置一个业务关键版本号。每次部署新的 bpmn 文件时,流程定义 ID 会变化,但流程定义 Key 不变。引擎会自动管理多个版本,新流程实例使用最新版本,老实例继续按旧版本流转直到结束。排查问题时,一定要按“流程实例 ID”查对应的流程定义版本,避免用错版本的 bpmn 文件来对问题,那样定位会非常困难。

部署后不要立即删除旧的 bpmn 文件。像项目审核这种场景,一个审核单可能横跨数月(比如年度预算审核),期间流程定义可能已经更新了两三个版本。旧实例运行时依赖的引擎表中,流程定义 ID 指向的是部署时的定义,如果清理了旧定义,历史流程无法继续操作。

4.2 权限校验失败与数据不可见的排查

权限相关的问题在项目上线初期尤其多,下面几个点是我在实际项目里反复踩过的坑。

第一个坑是用户上下文丢失。无论前端还是后端,只要有一步没把当前用户信息传到 intf 层,权限校验就会失败。表现是部分接口报无权限,部分接口又能通。排查方法是全局检查登录拦截器是否对所有入口统一生效,以及调用链中是否有没有经过拦截器的内部调用。

第二个坑是角色与权限点的匹配关系遗漏。新增角色后忘了给角色绑权限点,导致原本应该能审批的人,突然无法看到待办。排查方法是用管理员账号打开权限配置界面,逐个角色核对权限点绑定情况。最好能在每次发版前写一个权限配置自检脚本,检查角色是否为空权限,避免上线后才发现问题。

第三个坑是数据权限过滤器只过滤了部门维度,忽略了子部门。部门经理能看到自己部门和子部门的待办,但如果组织树的层级很深,需要有支持递归查询的实现方案。我在实际项目中用过于简单的前缀匹配方案,后来组织扩展到了四层,匹配逻辑就差点出问题。稳妥的做法是引入组织关系闭包表,一次性算清楚所有父级子级关系,逻辑清晰,性能也好。

4.3 操作日志丢失与异步写入问题

日志异步写入能提升主流程性能,但会引入新的风险点。最典型的问题是系统重启时,内存队列里尚未落库的日志会丢失。我的处理方案是双写策略:日志切面先把日志写入消息队列,再由消费者落库。消息队列自带持久化机制,即使应用重启,消息也不会丢。如果项目里没引入消息队列,也可以用本地消息表,先写日志表,再通过定时任务把日志状态从“待确认”更新为“已确认”。

另一个常见问题是日志详情超长导致数据库写入失败。操作详情是 JSON 格式,审核表单字段多的时候,一次详情可能有几十 KB。解决方法是把操作详情拆成两个字段:摘要字段,存操作结果和关键业务编号;详情字段,用 TEXT 或 JSONB 类型存完整快照。查询列表时只查摘要字段,点击详情时再查完整内容,减少资源消耗。

排查日志问题时,我建议在日志切面里加一个抽样器。默认记录全部日志,但发生异常时强制记录完整上下文,包括入参列表、方法名、耗时、异常堆栈。这样出问题时能快速定位是业务代码的原因,还是流程引擎的原因,还是权限的原因。

4.4 流程卡住的定位方法与处理策略

流程卡住是 bpmn 引擎项目里最常见的生产事故。表现是审核提交后,任务不见了,或者审批人一直没收到待办。

第一类卡住的原因是监听器抛异常。如果任务监听器的代码里有个空指针,引擎回滚了事务,但流程实例已经走到了这一步,界面显示还在上一个节点,再点办理又报错。排查时先查看引擎的日志表,找到最近的异常记录,定位到是哪个监听器类哪个方法抛的异常。针对监听器里的代码,建议所有异常都捕获并记录完整的流程实例 ID 和任务 ID,方便回放。

第二类卡住的原因是条件表达式计算结果不是布尔值。Flowable 的排他网关表达式必须返回布尔值,如果表达式写错了,或者流程变量没传,走到该节点时流程就会中断,且不会报明显错误。排查时检查流程实例的执行实例表,看当前停留在哪个节点,再对照 bpmn 文件里的条件表达式,确认流程变量是否满足条件。

第三类卡住的原因是指派人了但没指派到。这个运维排障时要重点看 bpmn 文件里的候选人和指派人的配置。如果用表达式动态指派,表达式返回空集合,引擎会创建一个没有执行人的任务,系统里看得到任务但谁都无法处理。解决办法是在流程设计时加一个兜底监听器,如果任务创建后发现候选人为空,自动转发给管理员,并记录一条告警日志。这类兜底逻辑建议在一开始就内置到公共封装里,避免上线后踩坑。

实际操作中还有一个高频场景:审批人点了同意,返回成功,但发起人没收到流程结束的提醒。这个通常不是引擎的问题,而是回调通知环节出了问题。intf 层办理任务成功后,需要发送事件通知。这类通知建议设计成可配置化,接邮件、企微、钉钉都行,但通知失败一定不能影响主流程,要用独立的异步任务去发,失败要有重试机制。

4.5 常见问题速查表

问题现象可能原因排查思路与处理方式
流程部署后新发起流程仍走旧规则未使用最新版本流程定义检查部署记录,确认流程定义 Key 对应最新版本 ID
审批人提交后任务直接消失网关条件表达式判断异常查看执行实例表,对照 bpmn 网关表达式检查流程变量
用户登录后待办列表为空,但明明有任务数据权限过滤条件过严用管理员账号模拟该用户,检查组织范围解析结果
报错无权限,但角色已绑定权限用户上下文未正确传递检查登录拦截器和内部调用链路是否传递用户信息
日志表数据量增长过快日志记录维度太粗增加分表策略,摘要字段和详情字段拆分存储
审批人离职导致流程卡住审批人硬编码在流程变量中改造为动态角色解析,增加管理员兜底转办功能
驳回后流程走向不对驳回目标节点设置错误在 intf 层的驳回接口中,明确区分驳回上一步和驳回到发起人
并行网关部分节点已完成但流程不往下走并行分支未全部到达汇合点查看执行实例表当前活动节点,检查是否有分支挂在等待

5. 总结我的实战经验与最后建议

整个项目做完,我最大的体会是:intf 对接 bpmn 的难点不在 API 调用,而在边界设计。intf 层必须想清楚哪些能力是对外暴露的,哪些是内部实现的。暴露得太多,业务方会绕过流程引擎直接操作底层数据,日志和权限就失控了;暴露得太少,业务方又会觉得难用,最后绕过 intf 层自己去调引擎,公共封装形同虚设。

权限和日志的封装同理,关键不是把代码写得多智能,而是把规则定清楚。权限的规则确定好后,要沉淀成文档和配置项;日志的规则确定好后,要统一字段规范和查询口径。这两块属于基础能力,越早统一越好。我曾见过一个项目,上线两年后想接操作日志中央审计,结果发现六个业务系统有五种日志格式,数据清洗的工作量足够一个团队干半年。

另外要提醒的是:公共封装一定要预留扩展点。比如日志记录,虽然默认实现是异步落数据库,但不同业务方可能有不同的日志存储需求,比如接 Elasticsearch 做全文检索。从这个角度出发,公共封装最好定义好日志写入的接口,把“默认实现”和“扩展实现”分离,这样后续演进不需要改动切面代码。

最后分享一个小技巧:bpmn 流程文件和公共封装的版本号,建议与业务系统的版本号解耦,单独管理。流程文件本身就有很强的不确定性,业务规则一变,可能这周就要重新部署一版。如果流程文件和业务代码耦合在一个发布单元里,流程调整会牵动业务系统发版,风险很大。把这套能力独立出来之后,权限配置、流程调整、日志配置都可以独立发布,运维会从容很多。这个设计带来的长期收益,远超过初期分模块的那点重构成本。

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

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

立即咨询