4
说明,本项目案例参考 https://gitee.com/smell2/ruoyi-vue-activiti 开源项目。

一、Activiti流程部署

当定义好流程之后,第一步就是要进行流程的部署操作,主要是采取读取bpmn资源文件的方法。

image.png

上传的流程定义:
image.png

查看流程图详情:
image.png

@Override
public void uploadStreamAndDeployment(MultipartFile file) throws IOException {
 // 获取上传的文件名
 String fileName = file.getOriginalFilename();
 // 获取输入流(字节流)对象
 InputStream fileInputStream = file.getInputStream();
 // 获取扩展名
 String extension = FilenameUtils.getExtension(fileName);
 // 初始化流程
 if(extension.equals("zip")) {
 ZipInputStream zipInputStream = new ZipInputStream(fileInputStream);
 repositoryService.createDeployment()
 .addZipInputStream(zipInputStream)
 .deploy();
 }else{
 repositoryService.createDeployment()
 .addInputStream(fileName, fileInputStream)
 .deploy();
 }
}

流程部署涉及到主要表:

  • act_ge_bytearray 二进制数据表
  • act_re_deployment 部署信息表
  • act_re_procdef 流程定义数据表

act_ge_bytearray 表:
image.png

字段 DEPLOYMENT_ID_ 关联 act_re_deployment 表的 ID_ 字段。

act_re_deployment 表:
image.png

act_re_procdef 表:
image.png

二、启动流程实例

1、启动实例

新增请假:

/**
 * 新增请假
 *
 * @param workflowLeave 请假
 * @return 结果
 */
@Override
public int insertWorkflowLeave(WorkflowLeave workflowLeave) {
 String id = UUID.randomUUID().toString();
 workflowLeave.setId(id);
 workflowLeave.setCreateTime(DateUtils.getNowDate());
 String join = StringUtils.join(sysUserService.selectUserNameByPostCodeAndDeptId("se", SecurityUtils.getLoginUser().getUser().getDeptId()), ",");
 ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder
            .start()
 .withProcessDefinitionKey("leave")
 .withName(workflowLeave.getTitle())
 .withBusinessKey(id)
 .withVariable("deptLeader",join)
 .build());
 workflowLeave.setInstanceId(processInstance.getId());
 workflowLeave.setState("0");
 workflowLeave.setCreateName(SecurityUtils.getNickName());
 workflowLeave.setCreateBy(SecurityUtils.getUsername());
 workflowLeave.setCreateTime(DateUtils.getNowDate());
 return workflowLeaveMapper.insertWorkflowLeave(workflowLeave);
}

workflow_leave 业务表:
image.png

启动实例ID:

InstanceId = 1dfab580-4218-11eb-ba85-005056c00001

涉及的表:

  • act_ru_execution: 代表正在执行的流程实例表,如果当期正在执行的流程实例结束以后,该行在这张表中就被删除掉了,所以该表也是一个临时表
  • act_ru_task: 代表正在执行的任务表,该表是一个临时表,如果当前任务被完成以后,任务在这张表中就被删除掉了
  • act_hi_actinst: 流程图上出现的每一个元素都称为activity,流程图上正在执行的元素或者已经执行完成的元素称为activity instance
  • act_hi_procinst: 流程历史表
  • act_hi_taskinst 历史任务表

act_ru_execution 表:
image.png

执行到第一个节点:
image.png

image.png

第一条记录为实例绑定具体的业务BUSINESS_KEY_,具体的业务名;
第二条记录为实例绑定【部门领导审批】节点,并且绑定节点 ACT_ID_=deptLeaderVer

SELECT 
ID_,PROC_INST_ID_,BUSINESS_KEY_,PARENT_ID_,PROC_DEF_ID_,ROOT_PROC_INST_ID_,ACT_ID_,IS_ACTIVE_,IS_SCOPE_,NAME_,START_TIME_ 
FROM `act_ru_execution` 

act_ru_task 表:
image.png

act_hi_actinst 表
image.png

说明:

***    act_hi_actinst
 *     1、说明
 *         act:activiti
 *         hi:history
 *         actinst:activity instance
 *            流程图上出现的每一个元素都称为activity
 *            流程图上正在执行的元素或者已经执行完成的元素称为activity instance
 *      2、字段
 *         proc_def_id:pdid
 *         proc_inst_id:流程实例ID
 *         execution_id_:执行ID
 *         act_id_:activity
 *         act_name
 *         act_type**

act_hi_procinst 表
image.png

说明:

 ***    act_hi_procinst
 *      1、说明
 *         procinst:process instance  历史的流程实例
 *            正在执行的流程实例也在这张表中
 *         如果end_time_为null,说明正在执行,如果有值,说明该流程实例已经结束了**

act_ru_identitylink 表

image.png

act_workflow_formdata 表
image.png

image.png

流程图文件:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.kafeitu.me/activiti/leave">
  <process id="leave" name="请假流程-普通表单" isExecutable="true">
    <documentation>请假流程演示</documentation>
    <startEvent id="startevent1" name="Start" />
    <userTask id="deptLeaderVerify" name="部门领导审批" activiti:formKey="deptLeaderVerify" activiti:candidateUsers="${deptLeader}">
      <extensionElements>
        <activiti:formProperty id="FormProperty_3qipis2--__!!radio--__!!审批意见--__!!i--__!!同意--__--不同意" type="string" />
        <activiti:formProperty id="FormProperty_0lffpcm--__!!textarea--__!!批注--__!!f__!!null" type="string" />
      </extensionElements>
    </userTask>
    <exclusiveGateway id="exclusivegateway5">
      <outgoing>Flow_0q3bbjl</outgoing>
    </exclusiveGateway>
    <userTask id="hrVerify" name="人事审批" activiti:formKey="hrVerify" activiti:candidateGroups="hr">
      <extensionElements>
        <activiti:formProperty id="FormProperty_23u95jb--__!!radio--__!!审批意见--__!!i--__!!同意--__--不同意" type="string" />
        <activiti:formProperty id="FormProperty_3t7tfkv--__!!textarea--__!!批注--__!!f--__!!null" type="string" />
      </extensionElements>
    </userTask>
    <exclusiveGateway id="exclusivegateway6">
      <outgoing>Flow_0p85954</outgoing>
      <outgoing>Flow_0ji7qcv</outgoing>
    </exclusiveGateway>
    <endEvent id="endevent1" name="End">
      <incoming>Flow_0p85954</incoming>
      <incoming>Flow_0ji7qcv</incoming>
      <incoming>Flow_0q3bbjl</incoming>
    </endEvent>
    <sequenceFlow id="flow2" sourceRef="startevent1" targetRef="deptLeaderVerify" />
    <sequenceFlow id="flow3" sourceRef="deptLeaderVerify" targetRef="exclusivegateway5" />
    <sequenceFlow id="flow5" name="同意" sourceRef="exclusivegateway5" targetRef="hrVerify">
      <conditionExpression xsi:type="tFormalExpression">${FormProperty_3qipis2==0}</conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="flow6" sourceRef="hrVerify" targetRef="exclusivegateway6" />
    <sequenceFlow id="Flow_0p85954" sourceRef="exclusivegateway6" targetRef="endevent1">
      <extensionElements>
        <activiti:executionListener class="com.ruoyi.leave.instener.LeaveEndStateListener" event="take">
          <activiti:field name="state">
            <activiti:string>1</activiti:string>
          </activiti:field>
        </activiti:executionListener>
      </extensionElements>
      <conditionExpression xsi:type="tFormalExpression">${FormProperty_23u95jb==0}</conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="Flow_0ji7qcv" sourceRef="exclusivegateway6" targetRef="endevent1">
      <extensionElements>
        <activiti:executionListener class="com.ruoyi.leave.instener.LeaveEndStateListener" event="take">
          <activiti:field name="state">
            <activiti:string>2</activiti:string>
          </activiti:field>
        </activiti:executionListener>
      </extensionElements>
    </sequenceFlow>
    <sequenceFlow id="Flow_0q3bbjl" sourceRef="exclusivegateway5" targetRef="endevent1">
      <extensionElements>
        <activiti:executionListener class="com.ruoyi.leave.instener.LeaveEndStateListener" event="take">
          <activiti:field name="state">
            <activiti:string>2</activiti:string>
          </activiti:field>
        </activiti:executionListener>
      </extensionElements>
    </sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_leave">
    <bpmndi:BPMNPlane id="BPMNPlane_leave" bpmnElement="leave">
      <bpmndi:BPMNEdge id="Flow_0q3bbjl_di" bpmnElement="Flow_0q3bbjl">
        <omgdi:waypoint x="260" y="83" />
        <omgdi:waypoint x="260" y="140" />
        <omgdi:waypoint x="582" y="140" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0ji7qcv_di" bpmnElement="Flow_0ji7qcv">
        <omgdi:waypoint x="505" y="83" />
        <omgdi:waypoint x="505" y="140" />
        <omgdi:waypoint x="582" y="140" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0p85954_di" bpmnElement="Flow_0p85954">
        <omgdi:waypoint x="525" y="63" />
        <omgdi:waypoint x="600" y="63" />
        <omgdi:waypoint x="600" y="122" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="BPMNEdge_flow6" bpmnElement="flow6">
        <omgdi:waypoint x="453" y="63" />
        <omgdi:waypoint x="485" y="63" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="BPMNEdge_flow5" bpmnElement="flow5">
        <omgdi:waypoint x="280" y="63" />
        <omgdi:waypoint x="348" y="63" />
        <bpmndi:BPMNLabel>
          <omgdc:Bounds x="300" y="46" width="22" height="11" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="BPMNEdge_flow3" bpmnElement="flow3">
        <omgdi:waypoint x="185" y="63" />
        <omgdi:waypoint x="240" y="63" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="BPMNEdge_flow2" bpmnElement="flow2">
        <omgdi:waypoint x="35" y="63" />
        <omgdi:waypoint x="80" y="63" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="BPMNShape_startevent1" bpmnElement="startevent1">
        <omgdc:Bounds x="0" y="46" width="35" height="35" />
        <bpmndi:BPMNLabel>
          <omgdc:Bounds x="5" y="81" width="25" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="BPMNShape_deptLeaderVerify" bpmnElement="deptLeaderVerify">
        <omgdc:Bounds x="80" y="36" width="105" height="55" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="BPMNShape_exclusivegateway5" bpmnElement="exclusivegateway5" isMarkerVisible="true">
        <omgdc:Bounds x="240" y="43" width="40" height="40" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="BPMNShape_hrVerify" bpmnElement="hrVerify">
        <omgdc:Bounds x="348" y="36" width="105" height="55" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="BPMNShape_exclusivegateway6" bpmnElement="exclusivegateway6" isMarkerVisible="true">
        <omgdc:Bounds x="485" y="43" width="40" height="40" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="BPMNShape_endevent1" bpmnElement="endevent1">
        <omgdc:Bounds x="582" y="122" width="35" height="35" />
        <bpmndi:BPMNLabel>
          <omgdc:Bounds x="590" y="157" width="20" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

三、详细解析Task任务(非常重要)

任务的概念:需要有人进行审批或者申请的为任务

任务的执行人的情况类型:

 情况一:当没有进入该节点之前,就可以确定任务的执行人
  实例:比如进行“请假申请”的流程时候,最开始执行的就是提交”请假申请“,那么就需要知道,谁提交的“请假”,很明显,在一个系统中,谁登陆到系统里面,谁就有提交“请假任务”的提交人,那么执行人就可以确定就是登录人。
情况二:有可能一个任务节点的执行人是固定的。

       实例:比如,在“公司财务报账”的流程中,最后一个审批的人,一定是财务部的最大的BOSS,所以,这样该流程的最后一个节点执行人,是不是就已经确定是为“财务部最大BOSS”了。

情况三:一个节点任务,之前是不存在执行人(未知),只有当符合身份的人,登陆系统,进入该系统,才能确定执行人。

实例:比如,如果当前的流程实例正在执行“自荐信审批”,这个时候,自荐信审批没有任务执行人,因为审批人是可以很多个,无法确定到底是谁,只有当咨询员登录系统以后才能给该任务赋值执行人,即存在只要是咨询员登陆,那么就可以看到所有的“自荐信”。

 情况四:一个任务节点有n多人能够执行该任务,但是只要有一个人执行完毕就完成该任务了:组任务

实例:比如,“进入地铁站通道”的流程,我们一般地铁都是有N个安全检查的入口,有很多个人在进行检查,那么我们要想通过检查,那么任意一个检察员只要通过即可,这就是组任务

详细分析:
情况一:
步骤:

(1)首先构建流程图:(注意区别上面的第一次画的内容)

image.png

image.png

image.png

image.png

(2)将bpmn的内容,生成一个png的图片(这个操作方法,上面都已经很详细了,不多说)

(3)代码实现步骤:

1:

/**
     * 部署流程
     */
    @Test
    public void startDeployTest(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        processEngine.getRepositoryService()
                .createDeployment()
                .name("请假流程:情况一")
                .addClasspathResource("com/hnu/scw/task/shenqing.bpmn")
                .deploy();
    }

数据库情况:

image.png

2:

/**
     * 启动流程实例
     *    可以设置一个流程变量
     */
    @Test
    public void testStartPI(){
        /**
         * 流程变量
         *   给<userTask id="请假申请" name="请假申请" activiti:assignee="#{student}"></userTask>
         *     的student赋值
         */
        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("student", "小明");
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        processEngine.getRuntimeService()
                .startProcessInstanceById("shenqing1:1:1304",variables);
    }

数据库情况:

image.png

分析:如果,我们安装下面的代码执行,那么就出出现如下的错误

 /**
     * 启动流程实例
     *    可以设置一个流程变量
     */
    @Test
    public void testStartPI(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        processEngine.getRuntimeService()
                .startProcessInstanceById("shenqing1:1:1304");
    }

image.png

原因:是否还记得,我们在画流程图的时候,对该请假申请的节点,分配了一个#{student},这个变量,这个其实含义就是说,当我们进行该节点的处理的时候,就需要分配一个执行人,如果没有分配,就会发生上面的错误。然后再回头想一下,是不是就是我们的第一种情况呢?因为,在进行请假的流程的执行开始的时候,其实申请人是已经可以确定了,就是登陆的用户

3:后面的代码如下:

/**
     * 在完成请假申请的任务的时候,给班主任审批的节点赋值任务的执行人
     */
    @Test
    public void testFinishTask_Teacher(){
        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("teacher", "我是小明的班主任");
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        processEngine.getTaskService()
                .complete("1405", variables); //完成任务的同时设置流程变量
    }
 
    /**
     * 在完成班主任审批的情况下,给教务处节点赋值
     */
    @Test
    public void testFinishTask_Manager(){
        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("manager", "我是小明的教务处处长");
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        processEngine.getTaskService()
                .complete("1603", variables); //完成任务的同时设置流程变量
    }
 
    /**
     * 结束流程实例
     */
    @Test
    public void testFinishTask(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        processEngine.getTaskService()
                .complete("1703");
    }

总结:针对情况一,那么我们必须要进入该节点执行前,就要分配一个执行人。

情况二:这个情况的话,这里不多介绍,因为之前的知识点中,都是在画流程图的时候就已经分配这个执行人了。可以回头去看看。

情况三:

步骤:(1)画流程图,这里不多介绍,就说一下需要修改的地方。
image.png

(2)编写的TaskListener监听类

package com.hnu.scw.tasklistener;
import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;
/**
 * @author Administrator
 * @create 2018-01-16 11:10
 * @desc tack任务的监听,主要是为了动态分配执行人
 **/
public class MyTaskListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        /**
         * 任务的执行人可以动态的赋值
         *   1、流程变量
         *        可以通过提取流程变量的方式给任务赋值执行人
         *   2、可以操作数据库
         *      方法一:(必须在web环境) WebApplicationContext ac = WebApplicationContextUtils
         *           .getWebApplicationContext(ServletActionContext.getServletContext());
                    xxxxService xxxxService = (xxxxService) ac.getBean("xxxxService");
                方法二:通过JDBC来进行数据库操作
         */
        //动态分配(这里是从上一节点中的tack变量的map中获取,只有流程没有结束,所有的变量都是可以获取)
        /*String value = (String)delegateTask.getVariable("aaa");
        delegateTask.setAssignee(value);*/
        //静态分配(用于确定该执行人就只有一种情况,是一种固定的)
        delegateTask.setAssignee("我是班主任");
    }
}

通过这样的方式的话,当有“请假申请”进行提交之后,“班主任”的这个节点,就会自动进行分配执行人。

情况四:

流程图如下:
image.png

具体的测试代码:(注意看写的注释内容,就明白了对应的数据库的什么表)

package com.hnu.scw.test;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.task.IdentityLink;
import org.activiti.engine.task.Task;
import org.junit.Test;
import java.util.List;
 
/**
 * @author scw
 * @create 2018-01-23 15:45
 * @desc 关于对于组任务的测试内容
 **/
public class GroupTaskTest {
    /**
     * 主要是对于某些任务流程中,有N个人,但是只需要其中的某一个通过,
     * 则该任务就通过了,所以针对这样的业务需求,就有如下的内容
     */
    @Test
    public void deployTashTest(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        processEngine.getRepositoryService()
                .createDeployment()
                .addClasspathResource("com/hnu/scw/test/task3.bpmn")
                .addClasspathResource("com/hnu/scw/test/task3.png")
                .name("组任务的测试")
                .deploy();
    }
    /**
     * 当启动完流程实例以后,进入了"电脑维修"节点,该节点是一个组任务
     *    这个时候,组任务的候选人就会被插入到两张表中
     *       act_ru_identitylink  存放的是当前正在执行的组任务的候选人
     *       act_hi_identitylink
     */
    @Test
    public void processTaskStartTest(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        processEngine.getRuntimeService()
                .startProcessInstanceByKey("task3");
    }
    /**
     * 对于act_hi_identitylink表,根据任务ID,即TASK_ID字段查询候选人
     */
    @Test
    public void testQueryCandidateByTaskId(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        List<IdentityLink> identityLinks = processEngine.getTaskService()
                .getIdentityLinksForTask("2104");
        for (IdentityLink identityLink : identityLinks) {
            System.out.println(identityLink.getUserId());
        }
    }
 
    /**
     * 对于act_hi_identitylink表,根据候选人,即USER_ID_查看组任务
     */
    @Test
    public void testQueryTaskByCandidate(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        List<Task> tasks = processEngine.getTaskService()
                .createTaskQuery()
                .taskCandidateUser("工程师1")
                .list();
        for (Task task : tasks) {
            System.out.println(task.getName());
        }
    }
    /**
     * 候选人中的其中一个人认领任务
     */
    @Test
    public void testClaimTask(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        processEngine.getTaskService()
                /**
                 * 第一个参数为taskId
                 * 第二个参数为认领人
                 */
                .claim("2104", "工程师2");
    }
 
}

四、附加知识点

1、Activiti关联业务

问题:Activiti里面本身自带有很多的数据表,它里面都是存在着关联关系,那么如何将其本身的表与我们的实际业务中的表进行关联呢?

解惑:其实,这个对于Activiti早已经想到这个问题,就是通过act_ru_exectution这个表中的business_key这个字段来进行关联。

实例分析:比如,针对上面的请假流程,那么,我们肯定在自己的业务中,就需要一张请假的信息表,比如,里面就包含,请假原因,请假人,请假时间等等基本请假信息。然后,我们其他的业务,也会根据这张表的内容,进行不断的扩充,比如,还需要记录对每条请假信息,每个审批节点中每个人的具体描述信息,那么这样就出现了一张“请假审批详细表”,很明显,这两张表就是通过“请假表中的主键ID”来进行关联的,那么就作为“请假详情表”中的外键。。。那么,同理,我们也是一样的,我们就通过对于act_ru_exectution这个数据表的business_key字段来关联着我们的业务主键即可。所以,这样就把我们自身的业务和Activiti进行了关联。

image.png

2、Activiti工作流的自带数据表的含义

(1)资源库流程规则表
1)act_re_deployment 部署信息表
2)act_re_model  流程设计模型部署表
3)act_re_procdef  流程定义数据表
(2):运行时数据库表
1)act_ru_execution 运行时流程执行实例表
2)act_ru_identitylink 运行时流程人员表,主要存储任务节点与参与者的相关信息
3)act_ru_task 运行时任务节点表
4)act_ru_variable 运行时流程变量数据表
(3):历史数据库表
1)act_hi_actinst 历史节点表
2)act_hi_attachment 历史附件表
3)act_hi_comment 历史意见表
4)act_hi_identitylink 历史流程人员表
5)act_hi_detail 历史详情表,提供历史变量的查询
6)act_hi_procinst 历史流程实例表
7)act_hi_taskinst 历史任务实例表
8)act_hi_varinst 历史变量表
(4):组织机构表
1)act_id_group 用户组信息表
2)act_id_info 用户扩展信息表
3)act_id_membership 用户与用户组对应信息表
4)act_id_user 用户信息表
这四张表很常见,基本的组织机构管理,关于用户认证方面建议还是自己开发一套,组件自带的功能太简单,使用中有很多需求难以满足 
(5):通用数据表
1)act_ge_bytearray 二进制数据表
2)act_ge_property 属性数据表存储整个流程引擎级别的数据,初始化表结构时,会默认插入三条记录,


相关文章:
手把手教你如何玩转Activiti工作流
Activiti就是这么简单


Corwien
6.3k 声望1.6k 粉丝

为者常成,行者常至。